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,489 @@
|
|
|
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
|
+
* ContextTracker — lightweight singleton that connects tool execution to playbook knowledge.
|
|
7
|
+
*
|
|
8
|
+
* Three jobs, each fires at the right moment:
|
|
9
|
+
* 1. DETECT context — on domain/app change (from tool params), cache matching playbook
|
|
10
|
+
* 2. GET hints — per tool call, return 0-2 relevant hint strings (0ms, in-memory)
|
|
11
|
+
* 3. COLLECT outcome — per tool call, push to in-memory buffer (no disk, no AI)
|
|
12
|
+
* 4. FLUSH — on session_release or every N actions, merge learnings into playbook
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
// ── Tool → error relevance mapping ──
|
|
17
|
+
// Maps tool names to the error context keywords that are relevant to them
|
|
18
|
+
const TOOL_ERROR_KEYWORDS = {
|
|
19
|
+
browser_click: ["click", "button", "element"],
|
|
20
|
+
browser_human_click: ["click", "button", "element"],
|
|
21
|
+
click: ["click", "button", "element"],
|
|
22
|
+
click_text: ["click", "button", "element"],
|
|
23
|
+
click_with_fallback: ["click", "button", "element"],
|
|
24
|
+
browser_type: ["type", "input", "form", "field", "value"],
|
|
25
|
+
type_text: ["type", "input", "form", "field", "value"],
|
|
26
|
+
type_with_fallback: ["type", "input", "form", "field", "value"],
|
|
27
|
+
browser_fill_form: ["form", "field", "input", "value"],
|
|
28
|
+
browser_navigate: ["navigate", "url", "page", "load"],
|
|
29
|
+
browser_dom: ["dom", "selector", "element"],
|
|
30
|
+
browser_js: ["script", "eval", "js"],
|
|
31
|
+
browser_wait: ["wait", "load", "timeout"],
|
|
32
|
+
scroll: ["scroll"],
|
|
33
|
+
scroll_with_fallback: ["scroll"],
|
|
34
|
+
};
|
|
35
|
+
// Tools that carry a URL in their params
|
|
36
|
+
const URL_TOOLS = new Set([
|
|
37
|
+
"browser_open", "browser_navigate",
|
|
38
|
+
]);
|
|
39
|
+
// Tools that carry a bundleId — native app tools where context should trigger
|
|
40
|
+
const BUNDLE_ID_TOOLS = new Set([
|
|
41
|
+
"focus", "launch", "ui_tree", "ui_find", "ui_press", "ui_set_value", "menu_click",
|
|
42
|
+
"click_with_fallback", "type_with_fallback", "read_with_fallback",
|
|
43
|
+
"locate_with_fallback", "select_with_fallback", "scroll_with_fallback",
|
|
44
|
+
"wait_for_state",
|
|
45
|
+
]);
|
|
46
|
+
// Tools that carry a target/selector in their params
|
|
47
|
+
const TARGET_PARAM_NAMES = ["selector", "target", "text", "label", "placeholder"];
|
|
48
|
+
const FLUSH_THRESHOLD = 50;
|
|
49
|
+
const MIN_OCCURRENCES_TO_PROMOTE = 2;
|
|
50
|
+
export class ContextTracker {
|
|
51
|
+
store;
|
|
52
|
+
execPlaybooksDir;
|
|
53
|
+
context = null;
|
|
54
|
+
learnings = [];
|
|
55
|
+
actionCount = 0;
|
|
56
|
+
appMap = null;
|
|
57
|
+
_currentPageContext = null;
|
|
58
|
+
_previousPageContext = null;
|
|
59
|
+
_pendingTransition = null;
|
|
60
|
+
constructor(store, execPlaybooksDir) {
|
|
61
|
+
this.store = store;
|
|
62
|
+
this.execPlaybooksDir = execPlaybooksDir;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Current page/view context derived from window title.
|
|
66
|
+
* Used by AppMap to place elements in page-specific zones
|
|
67
|
+
* instead of the flat "auto_discovered" bucket.
|
|
68
|
+
*/
|
|
69
|
+
get currentPageContext() {
|
|
70
|
+
return this._currentPageContext;
|
|
71
|
+
}
|
|
72
|
+
/** Set the AppMap instance for loading app mastery data on context change. */
|
|
73
|
+
setAppMap(map) {
|
|
74
|
+
this.appMap = map;
|
|
75
|
+
}
|
|
76
|
+
/** Get the current app mastery map data (if loaded). */
|
|
77
|
+
getAppMapData() {
|
|
78
|
+
return this.context?.appMapData ?? null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Update the page context from a window title.
|
|
82
|
+
* Called after each tool call with the focused window's title.
|
|
83
|
+
* Extracts the first segment (page/view name) for page-aware zone routing.
|
|
84
|
+
* Tracks transitions: when page changes, stores a consumable transition.
|
|
85
|
+
*/
|
|
86
|
+
updatePageContext(windowTitle) {
|
|
87
|
+
const oldPage = this._currentPageContext;
|
|
88
|
+
if (!windowTitle) {
|
|
89
|
+
this._currentPageContext = null;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const newPage = extractPageContext(windowTitle);
|
|
93
|
+
this._currentPageContext = newPage;
|
|
94
|
+
// Detect transition: both old and new must be non-null and different
|
|
95
|
+
if (oldPage && newPage && oldPage !== newPage) {
|
|
96
|
+
this._previousPageContext = oldPage;
|
|
97
|
+
this._pendingTransition = { from: oldPage, to: newPage };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Consume a pending page transition (returns null if no transition occurred).
|
|
102
|
+
* Each transition is consumed once — subsequent calls return null until the
|
|
103
|
+
* next page change.
|
|
104
|
+
*/
|
|
105
|
+
consumePageTransition() {
|
|
106
|
+
const transition = this._pendingTransition;
|
|
107
|
+
this._pendingTransition = null;
|
|
108
|
+
return transition;
|
|
109
|
+
}
|
|
110
|
+
// ═══════════════════════════════════════════════
|
|
111
|
+
// 1. DETECT — update context when domain changes
|
|
112
|
+
// ═══════════════════════════════════════════════
|
|
113
|
+
/**
|
|
114
|
+
* Call after every tool call. Extracts domain from URL params or
|
|
115
|
+
* bundleId from native tool params. Only does a playbook lookup
|
|
116
|
+
* when the context key actually changes.
|
|
117
|
+
*/
|
|
118
|
+
updateContext(toolName, params) {
|
|
119
|
+
// Path 1: URL-bearing tools (browser_open, browser_navigate)
|
|
120
|
+
if (URL_TOOLS.has(toolName)) {
|
|
121
|
+
const url = params.url;
|
|
122
|
+
if (!url)
|
|
123
|
+
return;
|
|
124
|
+
let domain;
|
|
125
|
+
try {
|
|
126
|
+
domain = new URL(url).hostname.replace(/^www\./, "");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!domain)
|
|
132
|
+
return; // javascript:, data:, blob: URLs have empty hostname
|
|
133
|
+
if (this.context?.domain === domain)
|
|
134
|
+
return;
|
|
135
|
+
const playbook = this.store.matchByDomain(domain);
|
|
136
|
+
this.context = buildCachedContext(domain, playbook);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Path 2: bundleId-bearing tools (native app automation)
|
|
140
|
+
if (BUNDLE_ID_TOOLS.has(toolName)) {
|
|
141
|
+
const rawBundleId = params.bundleId ?? params.pid;
|
|
142
|
+
if (!rawBundleId || typeof rawBundleId !== "string")
|
|
143
|
+
return;
|
|
144
|
+
const bundleId = rawBundleId;
|
|
145
|
+
const contextKey = `native:${bundleId}`;
|
|
146
|
+
if (this.context?.domain === contextKey)
|
|
147
|
+
return;
|
|
148
|
+
const playbook = this.store.matchByBundleId(bundleId);
|
|
149
|
+
this.context = buildCachedContext(contextKey, playbook);
|
|
150
|
+
// Load app mastery map on bundleId change
|
|
151
|
+
if (this.appMap) {
|
|
152
|
+
this.context.appMapData = this.appMap.load(bundleId) ?? null;
|
|
153
|
+
this.appMap.incrementSession(bundleId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Extract domain from a browser window title and load matching reference.
|
|
159
|
+
* Covers Safari and other non-CDP browsers where URL isn't in tool params.
|
|
160
|
+
*/
|
|
161
|
+
updateContextFromWindowTitle(bundleId, windowTitle) {
|
|
162
|
+
if (!windowTitle)
|
|
163
|
+
return;
|
|
164
|
+
// Only for known browser bundleIds
|
|
165
|
+
const BROWSER_BUNDLE_IDS = new Set([
|
|
166
|
+
"com.apple.Safari", "com.brave.Browser",
|
|
167
|
+
"org.chromium.Chromium", "com.vivaldi.Vivaldi",
|
|
168
|
+
"com.operasoftware.Opera",
|
|
169
|
+
]);
|
|
170
|
+
if (!BROWSER_BUNDLE_IDS.has(bundleId))
|
|
171
|
+
return;
|
|
172
|
+
// Try extracting domain from title patterns:
|
|
173
|
+
// "Page Title — Safari" or "Page Title - Safari" or URL directly in title
|
|
174
|
+
let domain = null;
|
|
175
|
+
// Pattern 1: title contains a URL-like segment
|
|
176
|
+
const urlMatch = windowTitle.match(/https?:\/\/([^/\s]+)/);
|
|
177
|
+
if (urlMatch?.[1]) {
|
|
178
|
+
domain = urlMatch[1].replace(/^www\./, "");
|
|
179
|
+
}
|
|
180
|
+
// Pattern 2: common "title — domain.com" or "title - domain.com — Browser"
|
|
181
|
+
if (!domain) {
|
|
182
|
+
// Strip trailing " — Safari", " - Brave" etc.
|
|
183
|
+
const stripped = windowTitle.replace(/\s*[—–-]\s*(Safari|Brave|Chromium|Vivaldi|Opera)\s*$/, "");
|
|
184
|
+
// Check if the last segment looks like a domain
|
|
185
|
+
const parts = stripped.split(/\s*[—–-]\s*/);
|
|
186
|
+
const lastPart = parts[parts.length - 1]?.trim() ?? "";
|
|
187
|
+
if (/^[a-z0-9-]+\.[a-z]{2,}$/i.test(lastPart)) {
|
|
188
|
+
domain = lastPart.toLowerCase();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!domain)
|
|
192
|
+
return;
|
|
193
|
+
if (this.context?.domain === domain)
|
|
194
|
+
return;
|
|
195
|
+
const playbook = this.store.matchByDomain(domain);
|
|
196
|
+
this.context = buildCachedContext(domain, playbook);
|
|
197
|
+
}
|
|
198
|
+
// ═══════════════════════════════════════════════
|
|
199
|
+
// 2. GET HINTS — 0-2 lines per tool call
|
|
200
|
+
// ═══════════════════════════════════════════════
|
|
201
|
+
/**
|
|
202
|
+
* Returns relevant hints for this tool call. Max 2 hints.
|
|
203
|
+
* Cost: map lookups only, ~0ms.
|
|
204
|
+
*/
|
|
205
|
+
getHints(toolName, params) {
|
|
206
|
+
if (!this.context?.playbook)
|
|
207
|
+
return [];
|
|
208
|
+
const hints = [];
|
|
209
|
+
// Check for known errors relevant to this tool
|
|
210
|
+
const errors = this.context.errorsByTool.get(toolName);
|
|
211
|
+
if (errors && errors.length > 0) {
|
|
212
|
+
// Pick highest severity error
|
|
213
|
+
const top = errors[0];
|
|
214
|
+
hints.push(`⚠ Known issue (${this.context.playbook.platform}): ${top.error} → ${top.solution}`);
|
|
215
|
+
}
|
|
216
|
+
// Check if there's a preferred selector for what the tool is targeting
|
|
217
|
+
if (hints.length < 2) {
|
|
218
|
+
const target = extractTarget(params);
|
|
219
|
+
if (target) {
|
|
220
|
+
// Look for a matching selector in playbook
|
|
221
|
+
const match = findRelevantSelector(target, this.context.allSelectors);
|
|
222
|
+
if (match) {
|
|
223
|
+
hints.push(`💡 Preferred selector (${this.context.playbook.platform}): ${match}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Check if an executable playbook exists in playbooks/ dir
|
|
228
|
+
if (hints.length < 2 && this.execPlaybooksDir) {
|
|
229
|
+
const pb = this.context.playbook;
|
|
230
|
+
const execPath = path.join(this.execPlaybooksDir, `${pb.id}.json`);
|
|
231
|
+
try {
|
|
232
|
+
if (fs.existsSync(execPath)) {
|
|
233
|
+
const execPb = JSON.parse(fs.readFileSync(execPath, "utf-8"));
|
|
234
|
+
if (Array.isArray(execPb.steps) && execPb.steps.length > 0) {
|
|
235
|
+
hints.push(`📋 Executable playbook "${pb.id}" has ${execPb.steps.length} steps. Use job_create(task=..., playbookId="${pb.id}") for auto-execution.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch { /* skip — don't break hints for a file read error */ }
|
|
240
|
+
}
|
|
241
|
+
// App mastery map hint
|
|
242
|
+
if (hints.length < 2 && this.context?.appMapData) {
|
|
243
|
+
const map = this.context.appMapData;
|
|
244
|
+
const zones = Object.keys(map.zones).length;
|
|
245
|
+
const verifiedPaths = map.navigationGraph.edges.filter((e) => e.verified).length;
|
|
246
|
+
const totalPaths = map.navigationGraph.edges.length;
|
|
247
|
+
const ratingDisplay = map.rating
|
|
248
|
+
? (map.rating.grade === "0" ? "0" : `${map.rating.grade}${map.rating.subTier}`)
|
|
249
|
+
: map.masteryLevel.toUpperCase();
|
|
250
|
+
// Include page-specific zone info if we have page context
|
|
251
|
+
let pageInfo = "";
|
|
252
|
+
if (this._currentPageContext) {
|
|
253
|
+
const pageZoneKey = `page::${this._currentPageContext}`;
|
|
254
|
+
const pageZone = map.zones[pageZoneKey];
|
|
255
|
+
if (pageZone) {
|
|
256
|
+
pageInfo = `, page "${this._currentPageContext}" ${pageZone.elements.length} els`;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
pageInfo = `, page "${this._currentPageContext}" (new)`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Navigation graph info
|
|
263
|
+
const navNodes = Object.keys(map.navigationGraph.nodes).length;
|
|
264
|
+
let navInfo = "";
|
|
265
|
+
if (navNodes > 0) {
|
|
266
|
+
navInfo = `, nav: ${navNodes} pages ${totalPaths} transitions`;
|
|
267
|
+
// Show outgoing edges from current page
|
|
268
|
+
if (this._currentPageContext) {
|
|
269
|
+
const outgoing = map.navigationGraph.edges.filter((e) => e.from === this._currentPageContext);
|
|
270
|
+
if (outgoing.length > 0) {
|
|
271
|
+
const destinations = outgoing
|
|
272
|
+
.slice(0, 3)
|
|
273
|
+
.map((e) => `${e.to} (${e.action})`)
|
|
274
|
+
.join(", ");
|
|
275
|
+
const more = outgoing.length > 3 ? ` +${outgoing.length - 3} more` : "";
|
|
276
|
+
navInfo += ` [from here: ${destinations}${more}]`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
hints.push(`🗺 Map: ${map.appName} — Rating ${ratingDisplay} ` +
|
|
281
|
+
`(${(map.confidence * 100).toFixed(0)}%, ${zones} zones, ` +
|
|
282
|
+
`${verifiedPaths}/${totalPaths} paths${pageInfo}${navInfo})`);
|
|
283
|
+
}
|
|
284
|
+
return hints;
|
|
285
|
+
}
|
|
286
|
+
// ═══════════════════════════════════════════════
|
|
287
|
+
// 3. COLLECT — record outcome in memory buffer
|
|
288
|
+
// ═══════════════════════════════════════════════
|
|
289
|
+
/**
|
|
290
|
+
* Record a tool outcome. Just an array push — no disk, no AI.
|
|
291
|
+
*/
|
|
292
|
+
recordOutcome(toolName, params, success, error) {
|
|
293
|
+
if (!this.context)
|
|
294
|
+
return;
|
|
295
|
+
this.learnings.push({
|
|
296
|
+
tool: toolName,
|
|
297
|
+
target: extractTarget(params),
|
|
298
|
+
domain: this.context.domain,
|
|
299
|
+
success,
|
|
300
|
+
error,
|
|
301
|
+
timestamp: new Date().toISOString(),
|
|
302
|
+
});
|
|
303
|
+
this.actionCount++;
|
|
304
|
+
// Auto-flush at threshold
|
|
305
|
+
if (this.actionCount >= FLUSH_THRESHOLD) {
|
|
306
|
+
this.flush();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// ═══════════════════════════════════════════════
|
|
310
|
+
// 4. FLUSH — merge learnings into playbook (one write)
|
|
311
|
+
// ═══════════════════════════════════════════════
|
|
312
|
+
/**
|
|
313
|
+
* Merge collected learnings into the matched playbook.
|
|
314
|
+
* Call on session_release or process exit.
|
|
315
|
+
* One disk write via PlaybookStore.save().
|
|
316
|
+
*/
|
|
317
|
+
flush() {
|
|
318
|
+
if (this.learnings.length === 0)
|
|
319
|
+
return;
|
|
320
|
+
if (!this.context?.playbook) {
|
|
321
|
+
this.learnings = [];
|
|
322
|
+
this.actionCount = 0;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const playbook = this.context.playbook;
|
|
326
|
+
let changed = false;
|
|
327
|
+
// ── Promote selectors that worked 2+ times ──
|
|
328
|
+
const selectorSuccessCount = new Map();
|
|
329
|
+
for (const l of this.learnings) {
|
|
330
|
+
if (l.success && l.target && /^[#.\[]|^[a-z]+[\[.#\s>+~]/.test(l.target) &&
|
|
331
|
+
!/\bon\w+\s*=/i.test(l.target)) {
|
|
332
|
+
const key = l.target;
|
|
333
|
+
selectorSuccessCount.set(key, (selectorSuccessCount.get(key) ?? 0) + 1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!playbook.selectors)
|
|
337
|
+
playbook.selectors = {};
|
|
338
|
+
if (!playbook.selectors["auto_discovered"])
|
|
339
|
+
playbook.selectors["auto_discovered"] = {};
|
|
340
|
+
for (const [selector, count] of selectorSuccessCount) {
|
|
341
|
+
if (count >= MIN_OCCURRENCES_TO_PROMOTE) {
|
|
342
|
+
// Don't overwrite existing selectors
|
|
343
|
+
const existing = this.context.allSelectors;
|
|
344
|
+
const alreadyKnown = [...existing.values()].some(s => s === selector);
|
|
345
|
+
if (!alreadyKnown) {
|
|
346
|
+
const key = `auto_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 5)}`;
|
|
347
|
+
playbook.selectors["auto_discovered"][key] = selector;
|
|
348
|
+
changed = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// ── Promote error patterns seen 2+ times with a common error message ──
|
|
353
|
+
const errorCounts = new Map();
|
|
354
|
+
for (const l of this.learnings) {
|
|
355
|
+
if (!l.success && l.error) {
|
|
356
|
+
const key = `${l.tool}::${l.error}`;
|
|
357
|
+
const existing = errorCounts.get(key);
|
|
358
|
+
if (existing) {
|
|
359
|
+
existing.count++;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
errorCounts.set(key, { count: 1, tool: l.tool, error: l.error });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!playbook.errors)
|
|
367
|
+
playbook.errors = [];
|
|
368
|
+
for (const [, { count, tool, error }] of errorCounts) {
|
|
369
|
+
if (count >= MIN_OCCURRENCES_TO_PROMOTE) {
|
|
370
|
+
// Don't add duplicates
|
|
371
|
+
const alreadyKnown = playbook.errors.some(e => e.error === error);
|
|
372
|
+
if (!alreadyKnown) {
|
|
373
|
+
playbook.errors.push({
|
|
374
|
+
error,
|
|
375
|
+
context: `tool: ${tool}, domain: ${this.context.domain}`,
|
|
376
|
+
solution: "No resolution yet — investigate and update this entry",
|
|
377
|
+
severity: count >= 4 ? "high" : "medium",
|
|
378
|
+
});
|
|
379
|
+
changed = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ── Save if changed ──
|
|
384
|
+
if (changed) {
|
|
385
|
+
this.store.save(playbook);
|
|
386
|
+
}
|
|
387
|
+
// Reset
|
|
388
|
+
this.learnings = [];
|
|
389
|
+
this.actionCount = 0;
|
|
390
|
+
}
|
|
391
|
+
/** Get the currently matched playbook (if any). */
|
|
392
|
+
getActivePlaybook() {
|
|
393
|
+
return this.context?.playbook ?? null;
|
|
394
|
+
}
|
|
395
|
+
/** Get the current domain being tracked. */
|
|
396
|
+
getCurrentDomain() {
|
|
397
|
+
return this.context?.domain ?? null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// ── Helpers ──
|
|
401
|
+
function buildCachedContext(domain, playbook) {
|
|
402
|
+
const errorsByTool = new Map();
|
|
403
|
+
const allSelectors = new Map();
|
|
404
|
+
if (playbook) {
|
|
405
|
+
// Index errors by relevant tool names
|
|
406
|
+
if (playbook.errors) {
|
|
407
|
+
for (const err of playbook.errors) {
|
|
408
|
+
const errLower = `${err.error} ${err.context} ${err.solution}`.toLowerCase();
|
|
409
|
+
for (const [tool, keywords] of Object.entries(TOOL_ERROR_KEYWORDS)) {
|
|
410
|
+
if (keywords.some(kw => errLower.includes(kw))) {
|
|
411
|
+
const existing = errorsByTool.get(tool) ?? [];
|
|
412
|
+
existing.push(err);
|
|
413
|
+
errorsByTool.set(tool, existing);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Sort each tool's errors by severity
|
|
418
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
419
|
+
for (const [tool, errors] of errorsByTool) {
|
|
420
|
+
errors.sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
|
|
421
|
+
errorsByTool.set(tool, errors);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Flatten all selectors into one map
|
|
425
|
+
if (playbook.selectors) {
|
|
426
|
+
for (const [group, sels] of Object.entries(playbook.selectors)) {
|
|
427
|
+
for (const [name, sel] of Object.entries(sels)) {
|
|
428
|
+
allSelectors.set(`${group}.${name}`, sel);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { domain, playbook, errorsByTool, allSelectors, appMapData: null };
|
|
434
|
+
}
|
|
435
|
+
function extractTarget(params) {
|
|
436
|
+
for (const name of TARGET_PARAM_NAMES) {
|
|
437
|
+
const val = params[name];
|
|
438
|
+
if (typeof val === "string" && val.length > 0)
|
|
439
|
+
return val;
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
function findRelevantSelector(target, selectors) {
|
|
444
|
+
if (selectors.size === 0 || !target)
|
|
445
|
+
return null;
|
|
446
|
+
const targetLower = target.toLowerCase();
|
|
447
|
+
// Check if any selector name loosely matches the target
|
|
448
|
+
for (const [name, sel] of selectors) {
|
|
449
|
+
const nameLower = name.toLowerCase();
|
|
450
|
+
// If target text matches a selector name (e.g., target="Search" matches "toolbar.search")
|
|
451
|
+
if (nameLower.includes(targetLower) || targetLower.includes(nameLower.split(".").pop() ?? "")) {
|
|
452
|
+
return `${name}: ${sel}`;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Extract a page/view context from a window title.
|
|
459
|
+
*
|
|
460
|
+
* Window titles commonly follow patterns like:
|
|
461
|
+
* "Tasks - My Workspace - Notion" → "Tasks"
|
|
462
|
+
* "Settings > General - MyApp" → "Settings > General"
|
|
463
|
+
* "Home | Slack" → "Home"
|
|
464
|
+
* "Untitled - Figma" → "Untitled"
|
|
465
|
+
* "MyApp" → "MyApp" (single segment, still useful)
|
|
466
|
+
*
|
|
467
|
+
* Strategy: split on common delimiters (" - ", " | ", " — "), take the first
|
|
468
|
+
* segment as the page context. This is intentionally simple and conservative.
|
|
469
|
+
*/
|
|
470
|
+
export function extractPageContext(windowTitle) {
|
|
471
|
+
if (!windowTitle || windowTitle.trim().length === 0)
|
|
472
|
+
return null;
|
|
473
|
+
const title = windowTitle.trim();
|
|
474
|
+
// Split on common title delimiters: " - ", " — ", " | "
|
|
475
|
+
const parts = title.split(/\s+[-—|]\s+/);
|
|
476
|
+
// Take the first segment — this is typically the page/view/document name
|
|
477
|
+
const page = parts[0]?.trim();
|
|
478
|
+
if (!page || page.length === 0)
|
|
479
|
+
return null;
|
|
480
|
+
// V5: Reject garbage — too short or consisting only of punctuation/delimiters
|
|
481
|
+
if (page.length < 2)
|
|
482
|
+
return null;
|
|
483
|
+
if (/^[\s\-—|_.,;:!?]+$/.test(page))
|
|
484
|
+
return null;
|
|
485
|
+
// Truncate overly long page contexts (window titles can be verbose)
|
|
486
|
+
if (page.length > 80)
|
|
487
|
+
return page.slice(0, 80);
|
|
488
|
+
return page;
|
|
489
|
+
}
|
|
@@ -14,18 +14,11 @@
|
|
|
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
17
|
import { TimelineLogger } from "./logging/timeline-logger.js";
|
|
19
18
|
import { MvpMcpServer } from "./mcp/server.js";
|
|
20
|
-
import {
|
|
21
|
-
type AppAdapter,
|
|
22
|
-
PlaceholderAppAdapter,
|
|
23
|
-
} from "./runtime/app-adapter.js";
|
|
19
|
+
import { PlaceholderAppAdapter, } from "./runtime/app-adapter.js";
|
|
24
20
|
import { CdpChromeAdapter } from "./runtime/cdp-chrome-adapter.js";
|
|
25
21
|
import { AutomationRuntimeService } from "./runtime/service.js";
|
|
26
|
-
|
|
27
|
-
// Re-export types and adapters for external use
|
|
28
|
-
export type { AppAdapter } from "./runtime/app-adapter.js";
|
|
29
22
|
export { PlaceholderAppAdapter } from "./runtime/app-adapter.js";
|
|
30
23
|
export { CdpChromeAdapter } from "./runtime/cdp-chrome-adapter.js";
|
|
31
24
|
export { AccessibilityAdapter } from "./runtime/accessibility-adapter.js";
|
|
@@ -37,56 +30,43 @@ export { StateObserver } from "./runtime/state-observer.js";
|
|
|
37
30
|
export { PlanningLoop } from "./runtime/planning-loop.js";
|
|
38
31
|
export { AutomationRuntimeService } from "./runtime/service.js";
|
|
39
32
|
export { MvpMcpServer } from "./mcp/server.js";
|
|
40
|
-
|
|
41
|
-
export
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
export { createMcpStdioServer, startMcpStdioServer } from "./mcp/mcp-stdio-server.js";
|
|
34
|
+
export { runAgentLoop } from "./agent/loop.js";
|
|
35
|
+
export function createRuntimeApp(adapter) {
|
|
36
|
+
const logger = new TimelineLogger();
|
|
37
|
+
const runtime = new AutomationRuntimeService(adapter, logger);
|
|
38
|
+
const mcp = new MvpMcpServer(runtime);
|
|
39
|
+
return { runtime, mcp };
|
|
44
40
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
async function createDefaultAdapter() {
|
|
42
|
+
if (process.env.AUTOMATOR_ADAPTER === "placeholder") {
|
|
43
|
+
return new PlaceholderAppAdapter();
|
|
44
|
+
}
|
|
45
|
+
if (process.env.AUTOMATOR_ADAPTER === "composite") {
|
|
46
|
+
// Lazy import to avoid requiring Swift bridge for CDP-only usage
|
|
47
|
+
const { MacOSBridgeClient } = await import("./native/macos-bridge-client.js");
|
|
48
|
+
const { CompositeAdapter } = await import("./runtime/composite-adapter.js");
|
|
49
|
+
const bridge = new MacOSBridgeClient();
|
|
50
|
+
return new CompositeAdapter(bridge, {
|
|
51
|
+
headless: process.env.AUTOMATOR_HEADLESS === "1",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (process.env.AUTOMATOR_ADAPTER === "accessibility") {
|
|
55
|
+
const { MacOSBridgeClient } = await import("./native/macos-bridge-client.js");
|
|
56
|
+
const { AccessibilityAdapter } = await import("./runtime/accessibility-adapter.js");
|
|
57
|
+
const bridge = new MacOSBridgeClient();
|
|
58
|
+
return new AccessibilityAdapter(bridge);
|
|
59
|
+
}
|
|
60
|
+
return new CdpChromeAdapter({
|
|
61
|
+
headless: process.env.AUTOMATOR_HEADLESS === "1",
|
|
64
62
|
});
|
|
65
|
-
}
|
|
66
|
-
if (process.env.AUTOMATOR_ADAPTER === "accessibility") {
|
|
67
|
-
const { MacOSBridgeClient } = await import("./native/macos-bridge-client.js");
|
|
68
|
-
const { AccessibilityAdapter } = await import("./runtime/accessibility-adapter.js");
|
|
69
|
-
const bridge = new MacOSBridgeClient();
|
|
70
|
-
return new AccessibilityAdapter(bridge);
|
|
71
|
-
}
|
|
72
|
-
return new CdpChromeAdapter({
|
|
73
|
-
headless: process.env.AUTOMATOR_HEADLESS === "1",
|
|
74
|
-
});
|
|
75
63
|
}
|
|
76
|
-
|
|
77
64
|
const app = createRuntimeApp(await createDefaultAdapter());
|
|
78
|
-
|
|
79
65
|
if (process.argv.includes("--healthcheck")) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
JSON.stringify(
|
|
83
|
-
{
|
|
66
|
+
const session = await app.runtime.sessionStart("automation");
|
|
67
|
+
console.log(JSON.stringify({
|
|
84
68
|
status: "ok",
|
|
85
69
|
session,
|
|
86
70
|
note: "Runtime loaded with universal adapter support.",
|
|
87
|
-
|
|
88
|
-
null,
|
|
89
|
-
2,
|
|
90
|
-
),
|
|
91
|
-
);
|
|
71
|
+
}, null, 2));
|
|
92
72
|
}
|