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.
Files changed (212) hide show
  1. package/README.md +165 -446
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +3615 -400
  4. package/dist/scripts/export-help-center.js +112 -0
  5. package/dist/scripts/marketing-loop.js +117 -0
  6. package/dist/scripts/observer-daemon.js +288 -0
  7. package/dist/scripts/orchestrator-daemon.js +399 -0
  8. package/dist/scripts/threads-campaign.js +208 -0
  9. package/dist/src/community/fetcher.js +109 -0
  10. package/dist/src/community/index.js +6 -0
  11. package/dist/src/community/publisher.js +191 -0
  12. package/dist/src/community/remote-api.js +121 -0
  13. package/dist/src/community/types.js +3 -0
  14. package/dist/src/community/validator.js +95 -0
  15. package/dist/src/context-tracker.js +489 -0
  16. package/dist/src/ingestion/coverage-auditor.js +233 -0
  17. package/dist/src/ingestion/doc-parser.js +164 -0
  18. package/dist/src/ingestion/index.js +8 -0
  19. package/dist/src/ingestion/menu-scanner.js +152 -0
  20. package/dist/src/ingestion/reference-merger.js +186 -0
  21. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  22. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  23. package/dist/src/ingestion/types.js +3 -0
  24. package/dist/src/jobs/manager.js +82 -14
  25. package/dist/src/jobs/runner.js +138 -15
  26. package/dist/src/learning/engine.js +356 -0
  27. package/dist/src/learning/index.js +9 -0
  28. package/dist/src/learning/locator-policy.js +120 -0
  29. package/dist/src/learning/pattern-policy.js +89 -0
  30. package/dist/src/learning/recovery-policy.js +116 -0
  31. package/dist/src/learning/sensor-policy.js +115 -0
  32. package/dist/src/learning/timing-model.js +204 -0
  33. package/dist/src/learning/topology-policy.js +90 -0
  34. package/dist/src/learning/types.js +9 -0
  35. package/dist/src/logging/timeline-logger.js +4 -1
  36. package/dist/src/memory/playbook-seeds.js +200 -0
  37. package/dist/src/memory/recall.js +60 -8
  38. package/dist/src/memory/service.js +30 -5
  39. package/dist/src/memory/store.js +34 -5
  40. package/dist/src/native/bridge-client.js +253 -31
  41. package/dist/src/observer/state.js +199 -0
  42. package/dist/src/observer/types.js +43 -0
  43. package/dist/src/orchestrator/state.js +68 -0
  44. package/dist/src/orchestrator/types.js +22 -0
  45. package/dist/src/perception/ax-source.js +162 -0
  46. package/dist/src/perception/cdp-source.js +162 -0
  47. package/dist/src/perception/coordinator.js +771 -0
  48. package/dist/src/perception/frame-differ.js +287 -0
  49. package/dist/src/perception/index.js +22 -0
  50. package/dist/src/perception/manager.js +199 -0
  51. package/dist/src/perception/types.js +47 -0
  52. package/dist/src/perception/vision-source.js +399 -0
  53. package/dist/src/planner/deterministic.js +298 -0
  54. package/dist/src/planner/executor.js +870 -0
  55. package/dist/src/planner/goal-store.js +92 -0
  56. package/dist/src/planner/index.js +21 -0
  57. package/dist/src/planner/planner.js +520 -0
  58. package/dist/src/planner/tool-registry.js +71 -0
  59. package/dist/src/planner/types.js +22 -0
  60. package/dist/src/platform/explorer.js +213 -0
  61. package/dist/src/platform/help-center-markdown.js +527 -0
  62. package/dist/src/platform/learner.js +257 -0
  63. package/dist/src/playbook/engine.js +296 -11
  64. package/dist/src/playbook/mcp-recorder.js +204 -0
  65. package/dist/src/playbook/recorder.js +3 -2
  66. package/dist/src/playbook/runner.js +1 -1
  67. package/dist/src/playbook/store.js +139 -10
  68. package/dist/src/recovery/detectors.js +156 -0
  69. package/dist/src/recovery/engine.js +327 -0
  70. package/dist/src/recovery/index.js +20 -0
  71. package/dist/src/recovery/strategies.js +274 -0
  72. package/dist/src/recovery/types.js +20 -0
  73. package/dist/src/runtime/accessibility-adapter.js +55 -18
  74. package/dist/src/runtime/applescript-adapter.js +8 -2
  75. package/dist/src/runtime/cdp-chrome-adapter.js +1 -1
  76. package/dist/src/runtime/executor.js +23 -3
  77. package/dist/src/runtime/locator-cache.js +24 -2
  78. package/dist/src/runtime/service.js +59 -15
  79. package/dist/src/runtime/session-manager.js +4 -1
  80. package/dist/src/runtime/vision-adapter.js +2 -1
  81. package/dist/src/state/app-map-types.js +72 -0
  82. package/dist/src/state/app-map.js +1974 -0
  83. package/dist/src/state/entity-tracker.js +108 -0
  84. package/dist/src/state/fusion.js +96 -0
  85. package/dist/src/state/index.js +21 -0
  86. package/dist/src/state/ladder-generator.js +236 -0
  87. package/dist/src/state/persistence.js +156 -0
  88. package/dist/src/state/types.js +17 -0
  89. package/dist/src/state/world-model.js +1456 -0
  90. package/dist/src/util/atomic-write.js +19 -4
  91. package/dist/src/util/sanitize.js +146 -0
  92. package/dist-app-maps/com.figma.Desktop.json +959 -0
  93. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  94. package/dist-app-maps/notion.id.json +2831 -0
  95. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  96. package/dist-playbooks/codex-desktop.json +76 -0
  97. package/dist-playbooks/competitor-research-stack.json +122 -0
  98. package/dist-playbooks/davinci-color-grade.json +153 -0
  99. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  100. package/dist-playbooks/davinci-render.json +114 -0
  101. package/dist-playbooks/devto.json +52 -0
  102. package/dist-playbooks/discord.json +41 -0
  103. package/dist-playbooks/google-flow-create-project.json +59 -0
  104. package/dist-playbooks/google-flow-edit-image.json +90 -0
  105. package/dist-playbooks/google-flow-edit-video.json +90 -0
  106. package/dist-playbooks/google-flow-generate-image.json +68 -0
  107. package/dist-playbooks/google-flow-generate-video.json +191 -0
  108. package/dist-playbooks/google-flow-open-project.json +48 -0
  109. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  110. package/dist-playbooks/google-flow-search-assets.json +64 -0
  111. package/dist-playbooks/instagram.json +57 -0
  112. package/dist-playbooks/linkedin.json +52 -0
  113. package/dist-playbooks/n8n.json +43 -0
  114. package/dist-playbooks/reddit.json +52 -0
  115. package/dist-playbooks/threads.json +59 -0
  116. package/dist-playbooks/x-twitter.json +59 -0
  117. package/dist-playbooks/youtube.json +59 -0
  118. package/dist-references/canva.json +646 -0
  119. package/dist-references/codex-desktop.json +305 -0
  120. package/dist-references/davinci-resolve-keyboard.json +594 -0
  121. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  122. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  123. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  124. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  125. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  126. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  127. package/dist-references/devpost.json +186 -0
  128. package/dist-references/devto.json +317 -0
  129. package/dist-references/discord.json +549 -0
  130. package/dist-references/figma.json +1186 -0
  131. package/dist-references/finder.json +146 -0
  132. package/dist-references/google-ads-transparency.json +95 -0
  133. package/dist-references/google-flow.json +649 -0
  134. package/dist-references/instagram.json +341 -0
  135. package/dist-references/linkedin.json +324 -0
  136. package/dist-references/meta-ad-library.json +86 -0
  137. package/dist-references/n8n.json +387 -0
  138. package/dist-references/notes.json +27 -0
  139. package/dist-references/notion.json +163 -0
  140. package/dist-references/reddit.json +341 -0
  141. package/dist-references/threads.json +337 -0
  142. package/dist-references/x-twitter.json +403 -0
  143. package/dist-references/youtube.json +373 -0
  144. package/native/macos-bridge/Package.swift +22 -0
  145. package/native/macos-bridge/Sources/AccessibilityBridge.swift +482 -0
  146. package/native/macos-bridge/Sources/AppManagement.swift +339 -0
  147. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +537 -0
  148. package/native/macos-bridge/Sources/ObserverBridge.swift +120 -0
  149. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  150. package/native/macos-bridge/Sources/VisionBridge.swift +238 -0
  151. package/native/macos-bridge/Sources/main.swift +498 -0
  152. package/native/windows-bridge/AppManagement.cs +234 -0
  153. package/native/windows-bridge/InputBridge.cs +436 -0
  154. package/native/windows-bridge/Program.cs +270 -0
  155. package/native/windows-bridge/ScreenCapture.cs +453 -0
  156. package/native/windows-bridge/UIAutomationBridge.cs +571 -0
  157. package/native/windows-bridge/WindowsBridge.csproj +17 -0
  158. package/package.json +12 -1
  159. package/scripts/postinstall.cjs +127 -0
  160. package/dist/.audit-log.jsonl +0 -55
  161. package/dist/.screenhand/memory/.lock +0 -1
  162. package/dist/.screenhand/memory/actions.jsonl +0 -85
  163. package/dist/.screenhand/memory/errors.jsonl +0 -5
  164. package/dist/.screenhand/memory/errors.jsonl.bak +0 -4
  165. package/dist/.screenhand/memory/state.json +0 -35
  166. package/dist/.screenhand/memory/state.json.bak +0 -35
  167. package/dist/.screenhand/memory/strategies.jsonl +0 -12
  168. package/dist/agent/cli.js +0 -73
  169. package/dist/agent/loop.js +0 -258
  170. package/dist/config.js +0 -9
  171. package/dist/index.js +0 -56
  172. package/dist/logging/timeline-logger.js +0 -29
  173. package/dist/mcp/mcp-stdio-server.js +0 -448
  174. package/dist/mcp/server.js +0 -347
  175. package/dist/mcp-entry.js +0 -59
  176. package/dist/memory/recall.js +0 -160
  177. package/dist/memory/research.js +0 -98
  178. package/dist/memory/seeds.js +0 -89
  179. package/dist/memory/session.js +0 -161
  180. package/dist/memory/store.js +0 -391
  181. package/dist/memory/types.js +0 -4
  182. package/dist/monitor/codex-monitor.js +0 -377
  183. package/dist/monitor/task-queue.js +0 -84
  184. package/dist/monitor/types.js +0 -49
  185. package/dist/native/bridge-client.js +0 -174
  186. package/dist/native/macos-bridge-client.js +0 -5
  187. package/dist/npm-publish-helper.js +0 -117
  188. package/dist/npm-token-cdp.js +0 -113
  189. package/dist/npm-token-create.js +0 -135
  190. package/dist/npm-token-finish.js +0 -126
  191. package/dist/playbook/engine.js +0 -193
  192. package/dist/playbook/index.js +0 -4
  193. package/dist/playbook/recorder.js +0 -519
  194. package/dist/playbook/runner.js +0 -392
  195. package/dist/playbook/store.js +0 -166
  196. package/dist/playbook/types.js +0 -4
  197. package/dist/runtime/accessibility-adapter.js +0 -377
  198. package/dist/runtime/app-adapter.js +0 -48
  199. package/dist/runtime/applescript-adapter.js +0 -283
  200. package/dist/runtime/ax-role-map.js +0 -80
  201. package/dist/runtime/browser-adapter.js +0 -36
  202. package/dist/runtime/cdp-chrome-adapter.js +0 -505
  203. package/dist/runtime/composite-adapter.js +0 -205
  204. package/dist/runtime/executor.js +0 -250
  205. package/dist/runtime/locator-cache.js +0 -12
  206. package/dist/runtime/planning-loop.js +0 -47
  207. package/dist/runtime/service.js +0 -372
  208. package/dist/runtime/session-manager.js +0 -28
  209. package/dist/runtime/state-observer.js +0 -105
  210. package/dist/runtime/vision-adapter.js +0 -208
  211. package/dist/test-mcp-protocol.js +0 -138
  212. package/dist/types.js +0 -1
@@ -0,0 +1,109 @@
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 * as os from "node:os";
6
+ import { RemoteCommunityAPI } from "./remote-api.js";
7
+ /**
8
+ * PlaybookFetcher — fetches community playbooks from local disk
9
+ * and optionally from a remote API (when SCREENHAND_COMMUNITY_URL is set).
10
+ *
11
+ * Local repo is always read. Remote results are merged and deduplicated.
12
+ */
13
+ export class PlaybookFetcher {
14
+ repoDir;
15
+ cache = null;
16
+ remote;
17
+ constructor(repoDir, remote) {
18
+ this.repoDir = repoDir ?? path.join(os.homedir(), ".screenhand", "community");
19
+ this.remote = remote ?? RemoteCommunityAPI.fromEnv();
20
+ }
21
+ /**
22
+ * Fetch community playbooks matching the query.
23
+ * Reads from local disk; async variant also merges remote results.
24
+ */
25
+ fetch(query) {
26
+ return this.filterAndRank(this.loadAll(), query);
27
+ }
28
+ /**
29
+ * Fetch with remote API merge (when SCREENHAND_COMMUNITY_URL is set).
30
+ * Local results are always included; remote results are deduplicated and merged.
31
+ */
32
+ async fetchWithRemote(query) {
33
+ const local = this.loadAll();
34
+ if (!this.remote) {
35
+ return this.filterAndRank(local, query);
36
+ }
37
+ try {
38
+ const remote = await this.remote.fetch(query);
39
+ // Deduplicate: local wins on ID collision
40
+ const localIds = new Set(local.map((pb) => pb.id));
41
+ const merged = [...local, ...remote.filter((pb) => !localIds.has(pb.id))];
42
+ return this.filterAndRank(merged, query);
43
+ }
44
+ catch {
45
+ return this.filterAndRank(local, query);
46
+ }
47
+ }
48
+ filterAndRank(all, query) {
49
+ return all
50
+ .filter((pb) => {
51
+ if (query.platform && pb.platform !== query.platform)
52
+ return false;
53
+ if (query.bundleId && pb.bundleId !== query.bundleId)
54
+ return false;
55
+ if (query.workflow) {
56
+ const lower = query.workflow.toLowerCase();
57
+ if (!pb.name.toLowerCase().includes(lower) &&
58
+ !pb.description.toLowerCase().includes(lower) &&
59
+ !pb.metadata.tags.some((t) => t.toLowerCase().includes(lower))) {
60
+ return false;
61
+ }
62
+ }
63
+ if (query.minScore !== undefined && pb.ratings.score < query.minScore)
64
+ return false;
65
+ return true;
66
+ })
67
+ .sort((a, b) => {
68
+ const scoreA = a.metadata.successRate * Math.max(a.ratings.score, 1);
69
+ const scoreB = b.metadata.successRate * Math.max(b.ratings.score, 1);
70
+ return scoreB - scoreA;
71
+ })
72
+ .slice(0, query.limit ?? 20);
73
+ }
74
+ /**
75
+ * Get a specific playbook by ID.
76
+ */
77
+ get(id) {
78
+ const all = this.loadAll();
79
+ return all.find((pb) => pb.id === id) ?? null;
80
+ }
81
+ /**
82
+ * Invalidate the cache (after a new playbook is published).
83
+ */
84
+ invalidateCache() {
85
+ this.cache = null;
86
+ }
87
+ loadAll() {
88
+ if (this.cache)
89
+ return this.cache;
90
+ const playbooks = [];
91
+ try {
92
+ if (!fs.existsSync(this.repoDir))
93
+ return playbooks;
94
+ const files = fs.readdirSync(this.repoDir);
95
+ for (const file of files) {
96
+ if (!file.endsWith(".json"))
97
+ continue;
98
+ try {
99
+ const raw = fs.readFileSync(path.join(this.repoDir, file), "utf-8");
100
+ playbooks.push(JSON.parse(raw));
101
+ }
102
+ catch { /* skip malformed */ }
103
+ }
104
+ }
105
+ catch { /* dir not found */ }
106
+ this.cache = playbooks;
107
+ return playbooks;
108
+ }
109
+ }
@@ -0,0 +1,6 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ export { PlaybookPublisher } from "./publisher.js";
4
+ export { PlaybookFetcher } from "./fetcher.js";
5
+ export { PlaybookValidator } from "./validator.js";
6
+ export { RemoteCommunityAPI } from "./remote-api.js";
@@ -0,0 +1,191 @@
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 * as os from "node:os";
6
+ import { writeFileAtomicSync } from "../util/atomic-write.js";
7
+ import { RemoteCommunityAPI } from "./remote-api.js";
8
+ /**
9
+ * PlaybookPublisher — prepares and publishes validated playbooks
10
+ * to a local shared repository and optionally to a remote API
11
+ * (when SCREENHAND_COMMUNITY_URL is set).
12
+ */
13
+ export class PlaybookPublisher {
14
+ repoDir;
15
+ remote;
16
+ constructor(repoDir, remote) {
17
+ this.repoDir = repoDir ?? path.join(os.homedir(), ".screenhand", "community");
18
+ this.remote = remote ?? RemoteCommunityAPI.fromEnv();
19
+ fs.mkdirSync(this.repoDir, { recursive: true });
20
+ }
21
+ /**
22
+ * Publish a validated local playbook to the community repository.
23
+ * Requires the playbook to have been run successfully at least minRuns times.
24
+ */
25
+ publish(playbook, successRate, executionCount, _minRuns) {
26
+ // Server-side minimum — cannot be overridden by caller
27
+ const MIN_RUNS = 3;
28
+ if (!Number.isFinite(executionCount) || executionCount < MIN_RUNS)
29
+ return null;
30
+ if (!Number.isFinite(successRate) || successRate < 0.5)
31
+ return null;
32
+ // Cross-check client-provided metrics against actual playbook data.
33
+ // ALWAYS use the playbook's own tracked counts — never trust client values.
34
+ // If the playbook has no tracked runs, it cannot be verified for publishing.
35
+ const actualRuns = playbook.successCount + playbook.failCount;
36
+ if (actualRuns < MIN_RUNS)
37
+ return null; // No tracked data = not publishable
38
+ const verifiedRate = playbook.successCount / actualRuns;
39
+ if (verifiedRate < 0.5)
40
+ return null;
41
+ const shared = {
42
+ id: `community_${playbook.id}_${Date.now().toString(36)}`,
43
+ name: playbook.name,
44
+ description: playbook.description,
45
+ platform: playbook.platform,
46
+ bundleId: playbook.bundleId ?? "",
47
+ version: "1.0.0",
48
+ steps: this.convertSteps(playbook.steps),
49
+ metadata: {
50
+ author: "anonymous",
51
+ publishedAt: new Date().toISOString(),
52
+ updatedAt: new Date().toISOString(),
53
+ os: process.platform,
54
+ successRate: verifiedRate,
55
+ executionCount: actualRuns,
56
+ tags: playbook.tags ?? [],
57
+ },
58
+ ratings: {
59
+ upvotes: 0,
60
+ downvotes: 0,
61
+ score: 0,
62
+ reportCount: 0,
63
+ },
64
+ };
65
+ // Strip sensitive data from params
66
+ for (const step of shared.steps) {
67
+ this.sanitizeParams(step.params);
68
+ }
69
+ // Sanitize ID to prevent path traversal
70
+ const safeId = shared.id.replace(/[^a-zA-Z0-9_\-]/g, "_");
71
+ const filePath = path.join(this.repoDir, `${safeId}.json`);
72
+ // Verify the file stays inside repoDir
73
+ const resolved = path.resolve(filePath);
74
+ if (!resolved.startsWith(path.resolve(this.repoDir))) {
75
+ return null;
76
+ }
77
+ writeFileAtomicSync(filePath, JSON.stringify(shared, null, 2) + "\n");
78
+ // Best-effort sync to remote API
79
+ if (this.remote) {
80
+ void this.remote.publish(shared).catch(() => { });
81
+ }
82
+ return shared;
83
+ }
84
+ /**
85
+ * List all published playbooks in the local repository.
86
+ */
87
+ list() {
88
+ const playbooks = [];
89
+ try {
90
+ const files = fs.readdirSync(this.repoDir);
91
+ for (const file of files) {
92
+ if (!file.endsWith(".json"))
93
+ continue;
94
+ try {
95
+ const raw = fs.readFileSync(path.join(this.repoDir, file), "utf-8");
96
+ playbooks.push(JSON.parse(raw));
97
+ }
98
+ catch { /* skip */ }
99
+ }
100
+ }
101
+ catch { /* dir not found */ }
102
+ return playbooks;
103
+ }
104
+ convertSteps(steps) {
105
+ return steps.map((step) => ({
106
+ action: step.action,
107
+ tool: this.actionToTool(step.action),
108
+ params: this.extractParams(step),
109
+ description: step.description ?? `${step.action} step`,
110
+ }));
111
+ }
112
+ extractParams(step) {
113
+ const params = {};
114
+ if (step.target !== undefined)
115
+ params.target = step.target;
116
+ if (step.text !== undefined)
117
+ params.text = step.text;
118
+ if (step.url !== undefined)
119
+ params.url = step.url;
120
+ if (step.keys !== undefined)
121
+ params.keys = step.keys;
122
+ if (step.menuPath !== undefined)
123
+ params.menuPath = step.menuPath;
124
+ if (step.ms !== undefined)
125
+ params.ms = step.ms;
126
+ if (step.direction !== undefined)
127
+ params.direction = step.direction;
128
+ if (step.amount !== undefined)
129
+ params.amount = step.amount;
130
+ // Normalize to MCP tool param shapes
131
+ if (Array.isArray(params.keys)) {
132
+ params.combo = params.keys.join("+");
133
+ delete params.keys;
134
+ }
135
+ if (Array.isArray(params.menuPath)) {
136
+ params.menuPath = params.menuPath.join("/");
137
+ }
138
+ return params;
139
+ }
140
+ actionToTool(action) {
141
+ switch (action) {
142
+ case "click": return "click_with_fallback";
143
+ case "type": return "type_with_fallback";
144
+ case "press": return "key";
145
+ case "navigate": return "browser_navigate";
146
+ case "wait": return "wait_for_state";
147
+ case "scroll": return "scroll_with_fallback";
148
+ default: return action;
149
+ }
150
+ }
151
+ /**
152
+ * Remove potentially sensitive values from params.
153
+ */
154
+ sanitizeParams(params) {
155
+ const sensitiveKeys = [
156
+ "password", "token", "secret", "credential",
157
+ "apikey", "api_key", "auth_token", "secret_key",
158
+ "access_key", "private_key",
159
+ ];
160
+ /** Patterns that indicate sensitive values regardless of key name */
161
+ const sensitiveValuePatterns = [
162
+ /sk-ant-api/i, // Anthropic API keys
163
+ /sk-[a-zA-Z0-9]{20,}/i, // OpenAI-style keys
164
+ /^ghp_[a-zA-Z0-9]{36}$/i, // GitHub PATs
165
+ /^xox[bpsar]-/i, // Slack tokens
166
+ /\bexport\s+\w*(?:KEY|TOKEN|SECRET|PASSWORD)\b/i, // env var exports
167
+ ];
168
+ for (const key of Object.keys(params)) {
169
+ if (sensitiveKeys.some((s) => key.toLowerCase() === s || key.toLowerCase().replace(/[^a-z_]/g, "") === s)) {
170
+ delete params[key];
171
+ continue;
172
+ }
173
+ const val = params[key];
174
+ // Check string values for sensitive patterns
175
+ if (typeof val === "string") {
176
+ if (sensitiveValuePatterns.some((p) => p.test(val))) {
177
+ delete params[key];
178
+ continue;
179
+ }
180
+ // Strip absolute file paths
181
+ if (val.startsWith("/") && (val.includes("/Users/") || val.includes("/home/"))) {
182
+ params[key] = path.basename(val);
183
+ }
184
+ }
185
+ // Recurse into nested objects
186
+ if (val && typeof val === "object" && !Array.isArray(val)) {
187
+ this.sanitizeParams(val);
188
+ }
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,121 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * RemoteCommunityAPI — optional remote backend for community playbook sharing.
5
+ *
6
+ * When `SCREENHAND_COMMUNITY_URL` is set, publish/fetch operations
7
+ * go to the remote API in addition to local disk. The local repo
8
+ * remains the source of truth; remote is best-effort sync.
9
+ *
10
+ * Expected API endpoints:
11
+ * POST /playbooks — publish a playbook
12
+ * GET /playbooks?... — search playbooks (query params from PlaybookQuery)
13
+ * GET /playbooks/:id — get a single playbook
14
+ * POST /playbooks/:id/rate — rate a playbook { score: 1 | -1 }
15
+ */
16
+ export class RemoteCommunityAPI {
17
+ baseUrl;
18
+ timeoutMs;
19
+ constructor(baseUrl, timeoutMs = 5_000) {
20
+ // Strip trailing slash
21
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
22
+ this.timeoutMs = timeoutMs;
23
+ }
24
+ /**
25
+ * Get the configured remote URL, or null if not configured.
26
+ */
27
+ static fromEnv() {
28
+ const url = process.env["SCREENHAND_COMMUNITY_URL"];
29
+ if (!url)
30
+ return null;
31
+ return new RemoteCommunityAPI(url);
32
+ }
33
+ /**
34
+ * Publish a playbook to the remote API.
35
+ * Returns the server-assigned ID, or null on failure.
36
+ */
37
+ async publish(playbook) {
38
+ try {
39
+ const res = await fetchWithTimeout(`${this.baseUrl}/playbooks`, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify(playbook),
43
+ }, this.timeoutMs);
44
+ if (!res.ok)
45
+ return null;
46
+ const body = (await res.json());
47
+ return body.id ?? playbook.id;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /**
54
+ * Fetch playbooks from the remote API matching a query.
55
+ */
56
+ async fetch(query) {
57
+ try {
58
+ const params = new URLSearchParams();
59
+ if (query.platform)
60
+ params.set("platform", query.platform);
61
+ if (query.bundleId)
62
+ params.set("bundleId", query.bundleId);
63
+ if (query.workflow)
64
+ params.set("workflow", query.workflow);
65
+ if (query.minScore !== undefined)
66
+ params.set("minScore", String(query.minScore));
67
+ if (query.limit !== undefined)
68
+ params.set("limit", String(query.limit));
69
+ const res = await fetchWithTimeout(`${this.baseUrl}/playbooks?${params.toString()}`, { method: "GET" }, this.timeoutMs);
70
+ if (!res.ok)
71
+ return [];
72
+ return (await res.json());
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ /**
79
+ * Get a specific playbook by ID.
80
+ */
81
+ async get(id) {
82
+ try {
83
+ const res = await fetchWithTimeout(`${this.baseUrl}/playbooks/${encodeURIComponent(id)}`, { method: "GET" }, this.timeoutMs);
84
+ if (!res.ok)
85
+ return null;
86
+ return (await res.json());
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * Rate a playbook (upvote/downvote).
94
+ */
95
+ async rate(id, score) {
96
+ try {
97
+ const res = await fetchWithTimeout(`${this.baseUrl}/playbooks/${encodeURIComponent(id)}/rate`, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({ score }),
101
+ }, this.timeoutMs);
102
+ return res.ok;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ }
109
+ /**
110
+ * Fetch with AbortController timeout.
111
+ */
112
+ async function fetchWithTimeout(url, init, timeoutMs) {
113
+ const controller = new AbortController();
114
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
115
+ try {
116
+ return await fetch(url, { ...init, signal: controller.signal });
117
+ }
118
+ finally {
119
+ clearTimeout(timer);
120
+ }
121
+ }
@@ -0,0 +1,3 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ export {};
@@ -0,0 +1,95 @@
1
+ // Copyright (C) 2025 Clazro Technology Private Limited
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ /**
4
+ * Tools that community playbooks are allowed to call.
5
+ * Excludes: applescript (arbitrary code exec), browser_js (XSS),
6
+ * browser_stealth (anti-detection), memory_* (data exfil),
7
+ * supervisor_* (system control), job_* (persistence).
8
+ */
9
+ const SAFE_TOOLS = new Set([
10
+ "click", "click_text", "click_with_fallback", "type_text", "type_with_fallback",
11
+ "key", "drag", "scroll", "scroll_with_fallback", "focus", "launch",
12
+ "screenshot", "screenshot_file", "ocr", "ui_tree", "ui_find", "ui_press",
13
+ "ui_set_value", "menu_click", "wait_for_state", "read_with_fallback",
14
+ "locate_with_fallback", "select_with_fallback",
15
+ "browser_open", "browser_navigate", "browser_click", "browser_type",
16
+ "browser_dom", "browser_wait", "browser_page_info", "browser_tabs",
17
+ "browser_fill_form", "browser_human_click",
18
+ "windows", "apps",
19
+ ]);
20
+ /**
21
+ * PlaybookValidator — tests a community playbook against the live app
22
+ * before accepting it into the local collection.
23
+ */
24
+ export class PlaybookValidator {
25
+ executeTool;
26
+ constructor(executeTool) {
27
+ this.executeTool = executeTool;
28
+ }
29
+ /**
30
+ * Validate a community playbook by executing its steps.
31
+ * Runs all steps and reports success/failure.
32
+ */
33
+ async validate(playbook) {
34
+ if (playbook.steps.length === 0) {
35
+ return {
36
+ playbook,
37
+ success: false,
38
+ stepsCompleted: 0,
39
+ totalSteps: 0,
40
+ errors: ["Playbook has no steps"],
41
+ validatedAt: new Date().toISOString(),
42
+ };
43
+ }
44
+ const errors = [];
45
+ let stepsCompleted = 0;
46
+ // Pre-validate: reject playbooks that use unsafe tools
47
+ for (const step of playbook.steps) {
48
+ if (!SAFE_TOOLS.has(step.tool)) {
49
+ return {
50
+ playbook,
51
+ success: false,
52
+ stepsCompleted: 0,
53
+ totalSteps: playbook.steps.length,
54
+ errors: [`Step "${step.description}" uses blocked tool "${step.tool}". Community playbooks cannot use this tool.`],
55
+ validatedAt: new Date().toISOString(),
56
+ };
57
+ }
58
+ }
59
+ for (const step of playbook.steps) {
60
+ try {
61
+ const result = await this.executeTool(step.tool, step.params);
62
+ if (result.ok) {
63
+ stepsCompleted++;
64
+ }
65
+ else {
66
+ errors.push(`Step ${stepsCompleted + 1} ("${step.description}") failed: ${result.error ?? "unknown error"}`);
67
+ break; // Stop on first failure
68
+ }
69
+ }
70
+ catch (err) {
71
+ errors.push(`Step ${stepsCompleted + 1} ("${step.description}") threw: ${err instanceof Error ? err.message : String(err)}`);
72
+ break;
73
+ }
74
+ }
75
+ return {
76
+ playbook,
77
+ success: stepsCompleted === playbook.steps.length,
78
+ stepsCompleted,
79
+ totalSteps: playbook.steps.length,
80
+ errors,
81
+ validatedAt: new Date().toISOString(),
82
+ };
83
+ }
84
+ /**
85
+ * Validate multiple playbooks and return the best one.
86
+ */
87
+ async findBest(playbooks) {
88
+ for (const pb of playbooks) {
89
+ const result = await this.validate(pb);
90
+ if (result.success)
91
+ return result;
92
+ }
93
+ return null;
94
+ }
95
+ }