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