screenhand 0.4.9 → 0.5.2
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/dist/mcp-desktop.js +297 -46
- package/dist/src/community/publisher.js +4 -2
- package/dist/src/context-tracker.js +36 -6
- package/dist/src/ingestion/reference-merger.js +33 -0
- package/dist/src/memory/recall.js +65 -1
- package/dist/src/memory/research.js +1 -1
- package/dist/src/memory/service.js +26 -5
- package/dist/src/memory/store.js +42 -23
- package/dist/src/native/bridge-client.js +3 -3
- package/dist/src/perception/coordinator.js +62 -15
- package/dist/src/perception/manager.js +65 -1
- package/dist/src/planner/executor.js +6 -2
- package/dist/src/planner/plan-refiner.js +213 -0
- package/dist/src/playbook/engine.js +18 -3
- package/dist/src/playbook/recorder.js +24 -8
- package/dist/src/playbook/runner.js +9 -3
- package/dist/src/playbook/store.js +8 -0
- package/dist/src/recovery/engine.js +9 -3
- package/dist/src/state/app-map.js +6 -2
- package/dist/src/state/state-watcher.js +144 -0
- package/dist/src/supervisor/supervisor.js +1 -1
- package/dist-app-maps/com.apple.iphonesimulator.json +714 -223
- package/dist-references/simulator.json +48 -2
- package/package.json +1 -1
|
@@ -150,7 +150,9 @@ export class PerceptionManager extends EventEmitter {
|
|
|
150
150
|
windowId = (frontmost ?? matching[0])?.windowId;
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
-
catch {
|
|
153
|
+
catch (e) {
|
|
154
|
+
process.stderr.write(`[perception-mgr] window ID lookup failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
155
|
+
}
|
|
154
156
|
const ctx = {
|
|
155
157
|
bundleId: focusedApp.bundleId,
|
|
156
158
|
appName: focusedApp.bundleId,
|
|
@@ -189,9 +191,44 @@ export class PerceptionManager extends EventEmitter {
|
|
|
189
191
|
notifyToolCall() {
|
|
190
192
|
this.coordinator?.notifyToolCall();
|
|
191
193
|
}
|
|
194
|
+
// ── Focus/Crash Tracking ──
|
|
195
|
+
expectedBundleId = null;
|
|
196
|
+
lastUIChangeTs = Date.now();
|
|
197
|
+
stallCheckInterval = null;
|
|
198
|
+
/** Set the expected focused app — enables focus_lost and app_crash detection. */
|
|
199
|
+
setExpectedApp(bundleId) {
|
|
200
|
+
this.expectedBundleId = bundleId;
|
|
201
|
+
this.lastUIChangeTs = Date.now();
|
|
202
|
+
}
|
|
203
|
+
/** Start stall detection (fires stall_detected if no UI changes for stallMs). */
|
|
204
|
+
startStallDetection(stallMs = 30_000) {
|
|
205
|
+
this.stopStallDetection();
|
|
206
|
+
this.lastUIChangeTs = Date.now();
|
|
207
|
+
this.stallCheckInterval = setInterval(() => {
|
|
208
|
+
if (!this.expectedBundleId)
|
|
209
|
+
return;
|
|
210
|
+
const elapsed = Date.now() - this.lastUIChangeTs;
|
|
211
|
+
if (elapsed >= stallMs) {
|
|
212
|
+
this.emit("stall_detected", {
|
|
213
|
+
bundleId: this.expectedBundleId,
|
|
214
|
+
stallMs: elapsed,
|
|
215
|
+
});
|
|
216
|
+
// Reset so we don't fire every interval tick
|
|
217
|
+
this.lastUIChangeTs = Date.now();
|
|
218
|
+
}
|
|
219
|
+
}, 5_000);
|
|
220
|
+
}
|
|
221
|
+
stopStallDetection() {
|
|
222
|
+
if (this.stallCheckInterval) {
|
|
223
|
+
clearInterval(this.stallCheckInterval);
|
|
224
|
+
this.stallCheckInterval = null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
192
227
|
handleReactiveEvent(event) {
|
|
193
228
|
if (event.data?.type === "ax_events" && Array.isArray(event.data.events)) {
|
|
194
229
|
for (const uiEvent of event.data.events) {
|
|
230
|
+
// Track any UI change for stall detection
|
|
231
|
+
this.lastUIChangeTs = Date.now();
|
|
195
232
|
if (uiEvent.type === "dialog_appeared") {
|
|
196
233
|
this.emit("dialog_detected", {
|
|
197
234
|
title: uiEvent.windowTitle ?? "",
|
|
@@ -205,6 +242,33 @@ export class PerceptionManager extends EventEmitter {
|
|
|
205
242
|
bundleId: uiEvent.bundleId,
|
|
206
243
|
pid: uiEvent.pid,
|
|
207
244
|
});
|
|
245
|
+
// Focus loss detection: if we expected a specific app and a different one took focus
|
|
246
|
+
if (this.expectedBundleId && uiEvent.bundleId !== this.expectedBundleId) {
|
|
247
|
+
this.emit("focus_lost", {
|
|
248
|
+
expectedBundleId: this.expectedBundleId,
|
|
249
|
+
actualBundleId: uiEvent.bundleId,
|
|
250
|
+
pid: uiEvent.pid,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// App crash detection: when expected app deactivates, check if PID is still alive.
|
|
255
|
+
// Only emit app_crash if the process is actually gone (not just lost focus).
|
|
256
|
+
if (uiEvent.type === "app_deactivated" &&
|
|
257
|
+
uiEvent.bundleId &&
|
|
258
|
+
uiEvent.bundleId === this.expectedBundleId &&
|
|
259
|
+
uiEvent.pid) {
|
|
260
|
+
try {
|
|
261
|
+
// process.kill(pid, 0) throws if PID doesn't exist
|
|
262
|
+
process.kill(uiEvent.pid, 0);
|
|
263
|
+
// PID alive — just a focus switch, handled by focus_lost above
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// PID dead — app crashed
|
|
267
|
+
this.emit("app_crash", {
|
|
268
|
+
bundleId: uiEvent.bundleId,
|
|
269
|
+
pid: uiEvent.pid,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
208
272
|
}
|
|
209
273
|
}
|
|
210
274
|
}
|
|
@@ -429,7 +429,9 @@ export class PlanExecutor {
|
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
|
-
catch {
|
|
432
|
+
catch (e) {
|
|
433
|
+
process.stderr.write(`[planner] contract precondition check failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
434
|
+
}
|
|
433
435
|
}
|
|
434
436
|
}
|
|
435
437
|
// 4. Focus validation: for type_text, verify a text field is focused
|
|
@@ -785,7 +787,9 @@ export class PlanExecutor {
|
|
|
785
787
|
}
|
|
786
788
|
}
|
|
787
789
|
}
|
|
788
|
-
catch {
|
|
790
|
+
catch (e) {
|
|
791
|
+
process.stderr.write(`[planner] contract outcome inference failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
792
|
+
}
|
|
789
793
|
}
|
|
790
794
|
}
|
|
791
795
|
// Navigation or state-changing click → next step's target should be visible
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
/**
|
|
18
|
+
* PlanRefiner — Self-improving plans.
|
|
19
|
+
*
|
|
20
|
+
* After a successful plan execution, diffs the original plan vs what actually
|
|
21
|
+
* happened (steps skipped, fallbacks used, extra retries). Produces a refined
|
|
22
|
+
* plan stripped of dead steps. After 3 successful refinements of the same goal
|
|
23
|
+
* pattern, graduates the plan to a playbook.
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from "node:fs";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { writeFileAtomicSync } from "../util/atomic-write.js";
|
|
28
|
+
const GRADUATION_THRESHOLD = 3;
|
|
29
|
+
const MAX_REFINED_PLANS = 200;
|
|
30
|
+
export class PlanRefiner {
|
|
31
|
+
stateDir;
|
|
32
|
+
cache = new Map();
|
|
33
|
+
filePath;
|
|
34
|
+
constructor(stateDir) {
|
|
35
|
+
this.stateDir = stateDir;
|
|
36
|
+
this.filePath = path.join(stateDir, "refined-plans.json");
|
|
37
|
+
this.load();
|
|
38
|
+
}
|
|
39
|
+
load() {
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(this.filePath)) {
|
|
42
|
+
const data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
|
|
43
|
+
if (Array.isArray(data)) {
|
|
44
|
+
for (const entry of data) {
|
|
45
|
+
if (entry.goalKey && typeof entry.goalKey === "string") {
|
|
46
|
+
this.cache.set(entry.goalKey, entry);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Corrupted file — start fresh
|
|
54
|
+
this.cache.clear();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
save() {
|
|
58
|
+
try {
|
|
59
|
+
// Evict oldest graduated plans if over limit
|
|
60
|
+
if (this.cache.size > MAX_REFINED_PLANS) {
|
|
61
|
+
const sorted = [...this.cache.entries()]
|
|
62
|
+
.sort((a, b) => new Date(a[1].lastRefinedAt).getTime() - new Date(b[1].lastRefinedAt).getTime());
|
|
63
|
+
while (this.cache.size > MAX_REFINED_PLANS && sorted.length > 0) {
|
|
64
|
+
const oldest = sorted.shift();
|
|
65
|
+
this.cache.delete(oldest[0]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const data = [...this.cache.values()];
|
|
69
|
+
writeFileAtomicSync(this.filePath, JSON.stringify(data, null, 2));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
process.stderr.write(`[plan-refiner] Save failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
normalizeGoal(description) {
|
|
76
|
+
return description.toLowerCase().trim().replace(/\s+/g, " ");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Refine a plan after successful execution.
|
|
80
|
+
* Extracts only the steps that actually completed (strips skipped/failed).
|
|
81
|
+
* Returns the refinement count and whether the plan was graduated.
|
|
82
|
+
*/
|
|
83
|
+
refine(goal, result) {
|
|
84
|
+
if (!result.success)
|
|
85
|
+
return { refinementCount: 0, graduated: false };
|
|
86
|
+
const goalKey = this.normalizeGoal(goal.description);
|
|
87
|
+
// Extract completed steps from all subgoals
|
|
88
|
+
const completedSteps = [];
|
|
89
|
+
for (const sg of goal.subgoals) {
|
|
90
|
+
if (sg.plan) {
|
|
91
|
+
for (const step of sg.plan.steps) {
|
|
92
|
+
if (step.status === "completed") {
|
|
93
|
+
completedSteps.push({ tool: step.tool, params: step.params });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (completedSteps.length === 0)
|
|
99
|
+
return { refinementCount: 0, graduated: false };
|
|
100
|
+
const existing = this.cache.get(goalKey);
|
|
101
|
+
const refinementCount = (existing?.refinementCount ?? 0) + 1;
|
|
102
|
+
const prevAvg = existing?.avgDurationMs ?? result.durationMs;
|
|
103
|
+
const avgDurationMs = Math.round((prevAvg * (refinementCount - 1) + result.durationMs) / refinementCount);
|
|
104
|
+
const refined = {
|
|
105
|
+
goalKey,
|
|
106
|
+
goalDescription: goal.description,
|
|
107
|
+
steps: completedSteps,
|
|
108
|
+
refinementCount,
|
|
109
|
+
avgDurationMs,
|
|
110
|
+
lastRefinedAt: new Date().toISOString(),
|
|
111
|
+
graduated: existing?.graduated ?? false,
|
|
112
|
+
};
|
|
113
|
+
this.cache.set(goalKey, refined);
|
|
114
|
+
this.save();
|
|
115
|
+
return { refinementCount, graduated: refined.graduated };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if a goal should graduate to a playbook.
|
|
119
|
+
* Returns the playbook if ready, null otherwise.
|
|
120
|
+
*/
|
|
121
|
+
checkGraduation(goalDescription, bundleId, appName) {
|
|
122
|
+
const goalKey = this.normalizeGoal(goalDescription);
|
|
123
|
+
const refined = this.cache.get(goalKey);
|
|
124
|
+
if (!refined)
|
|
125
|
+
return null;
|
|
126
|
+
if (refined.graduated)
|
|
127
|
+
return null;
|
|
128
|
+
if (refined.refinementCount < GRADUATION_THRESHOLD)
|
|
129
|
+
return null;
|
|
130
|
+
// Graduate: create a playbook from the refined steps
|
|
131
|
+
const playbookSteps = [];
|
|
132
|
+
for (let i = 0; i < refined.steps.length; i++) {
|
|
133
|
+
const s = refined.steps[i];
|
|
134
|
+
const mapped = mapToolToPlaybookStep(s.tool, s.params, i);
|
|
135
|
+
if (mapped)
|
|
136
|
+
playbookSteps.push(mapped);
|
|
137
|
+
}
|
|
138
|
+
const playbook = {
|
|
139
|
+
id: `graduated_${goalKey.replace(/[^a-z0-9]/g, "_").slice(0, 40)}_${Date.now().toString(36)}`,
|
|
140
|
+
name: refined.goalDescription,
|
|
141
|
+
description: `Auto-graduated from ${refined.refinementCount} successful plan executions (avg ${refined.avgDurationMs}ms)`,
|
|
142
|
+
platform: appName.toLowerCase(),
|
|
143
|
+
bundleId,
|
|
144
|
+
version: "1.0.0",
|
|
145
|
+
steps: playbookSteps,
|
|
146
|
+
tags: [appName.toLowerCase(), "auto-graduated", "refined"],
|
|
147
|
+
successCount: refined.refinementCount,
|
|
148
|
+
failCount: 0,
|
|
149
|
+
selectors: {},
|
|
150
|
+
errors: [],
|
|
151
|
+
};
|
|
152
|
+
// Mark as graduated
|
|
153
|
+
refined.graduated = true;
|
|
154
|
+
this.save();
|
|
155
|
+
return playbook;
|
|
156
|
+
}
|
|
157
|
+
/** Get all refined plans (for debugging/status). */
|
|
158
|
+
getAll() {
|
|
159
|
+
return [...this.cache.values()];
|
|
160
|
+
}
|
|
161
|
+
/** Look up a refined plan for a goal. */
|
|
162
|
+
lookup(goalDescription) {
|
|
163
|
+
return this.cache.get(this.normalizeGoal(goalDescription)) ?? null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Map MCP tool name + params to a PlaybookStep. Returns null if unmappable. */
|
|
167
|
+
function mapToolToPlaybookStep(tool, params, index) {
|
|
168
|
+
const TOOL_TO_ACTION = {
|
|
169
|
+
ui_press: "press",
|
|
170
|
+
click_text: "press",
|
|
171
|
+
click: "press",
|
|
172
|
+
click_with_fallback: "press",
|
|
173
|
+
type_text: "type_into",
|
|
174
|
+
type_with_fallback: "type_into",
|
|
175
|
+
key: "key",
|
|
176
|
+
browser_navigate: "navigate",
|
|
177
|
+
browser_click: "browser_click",
|
|
178
|
+
browser_type: "browser_type",
|
|
179
|
+
browser_human_click: "browser_human_click",
|
|
180
|
+
browser_js: "browser_js",
|
|
181
|
+
menu_click: "menu_click",
|
|
182
|
+
scroll: "scroll",
|
|
183
|
+
scroll_with_fallback: "scroll",
|
|
184
|
+
screenshot: "screenshot",
|
|
185
|
+
applescript: "applescript",
|
|
186
|
+
};
|
|
187
|
+
const action = TOOL_TO_ACTION[tool];
|
|
188
|
+
if (!action)
|
|
189
|
+
return null; // focus, launch, etc. are not playbook-able
|
|
190
|
+
const step = {
|
|
191
|
+
action,
|
|
192
|
+
description: `Step ${index + 1}: ${tool}`,
|
|
193
|
+
};
|
|
194
|
+
// Map params to playbook step fields
|
|
195
|
+
const target = params.title ?? params.text ?? params.selector ?? params.target;
|
|
196
|
+
if (typeof target === "string")
|
|
197
|
+
step.target = target;
|
|
198
|
+
if (typeof params.text === "string")
|
|
199
|
+
step.text = params.text;
|
|
200
|
+
if (typeof params.url === "string")
|
|
201
|
+
step.url = params.url;
|
|
202
|
+
if (typeof params.combo === "string")
|
|
203
|
+
step.keys = [params.combo];
|
|
204
|
+
if (typeof params.key === "string")
|
|
205
|
+
step.keys = [params.key];
|
|
206
|
+
if (Array.isArray(params.menuPath))
|
|
207
|
+
step.menuPath = params.menuPath;
|
|
208
|
+
if (typeof params.direction === "string")
|
|
209
|
+
step.direction = params.direction;
|
|
210
|
+
if (typeof params.amount === "number")
|
|
211
|
+
step.amount = params.amount;
|
|
212
|
+
return step;
|
|
213
|
+
}
|
|
@@ -23,9 +23,15 @@ export class PlaybookEngine {
|
|
|
23
23
|
appleScriptRunner;
|
|
24
24
|
/** Enable observer-based popup checks before each step */
|
|
25
25
|
popupCheckEnabled = false;
|
|
26
|
+
/** Optional callback invoked after each step with outcome — wires into learning/memory. */
|
|
27
|
+
onOutcome;
|
|
26
28
|
constructor(runtime) {
|
|
27
29
|
this.runtime = runtime;
|
|
28
30
|
}
|
|
31
|
+
/** Set callback for step outcomes so PlaybookEngine feeds learning. */
|
|
32
|
+
setOutcomeCallback(cb) {
|
|
33
|
+
this.onOutcome = cb;
|
|
34
|
+
}
|
|
29
35
|
/** Enable/disable pre-step popup detection via observer daemon */
|
|
30
36
|
setPopupCheck(enabled) {
|
|
31
37
|
this.popupCheckEnabled = enabled;
|
|
@@ -61,6 +67,9 @@ export class PlaybookEngine {
|
|
|
61
67
|
}
|
|
62
68
|
const result = await this.executeStep(sessionId, step, playbook.cdpPort);
|
|
63
69
|
stepsCompleted++;
|
|
70
|
+
// Report success to learning pipeline
|
|
71
|
+
if (this.onOutcome)
|
|
72
|
+
this.onOutcome(step, true, null);
|
|
64
73
|
if (options.onStep) {
|
|
65
74
|
options.onStep(i, step, result);
|
|
66
75
|
}
|
|
@@ -68,6 +77,8 @@ export class PlaybookEngine {
|
|
|
68
77
|
if (step.verify) {
|
|
69
78
|
const verified = await this.verifyStep(sessionId, step);
|
|
70
79
|
if (!verified && !step.optional) {
|
|
80
|
+
if (this.onOutcome)
|
|
81
|
+
this.onOutcome(step, false, `Verification failed at step ${i}`);
|
|
71
82
|
return {
|
|
72
83
|
playbook: playbook.id,
|
|
73
84
|
success: false,
|
|
@@ -83,10 +94,14 @@ export class PlaybookEngine {
|
|
|
83
94
|
await sleep(STEP_DELAY_MS);
|
|
84
95
|
}
|
|
85
96
|
catch (err) {
|
|
97
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
// Report failure to learning pipeline
|
|
99
|
+
if (this.onOutcome)
|
|
100
|
+
this.onOutcome(step, false, errMsg);
|
|
86
101
|
if (step.optional) {
|
|
87
102
|
stepsCompleted++;
|
|
88
103
|
if (options.onStep) {
|
|
89
|
-
options.onStep(i, step, `Skipped (optional): ${
|
|
104
|
+
options.onStep(i, step, `Skipped (optional): ${errMsg}`);
|
|
90
105
|
}
|
|
91
106
|
continue;
|
|
92
107
|
}
|
|
@@ -96,7 +111,7 @@ export class PlaybookEngine {
|
|
|
96
111
|
stepsCompleted,
|
|
97
112
|
totalSteps: playbook.steps.length,
|
|
98
113
|
failedAtStep: i,
|
|
99
|
-
error:
|
|
114
|
+
error: errMsg,
|
|
100
115
|
durationMs: Date.now() - start,
|
|
101
116
|
};
|
|
102
117
|
}
|
|
@@ -115,7 +130,7 @@ export class PlaybookEngine {
|
|
|
115
130
|
*/
|
|
116
131
|
/** Tools that the PlaybookEngine refuses to execute (defense in depth). */
|
|
117
132
|
static BLOCKED_ACTIONS = new Set([
|
|
118
|
-
"
|
|
133
|
+
"browser_stealth",
|
|
119
134
|
"memory_save", "memory_clear", "memory_snapshot",
|
|
120
135
|
"supervisor_start", "supervisor_stop", "supervisor_install", "supervisor_uninstall",
|
|
121
136
|
"job_create", "worker_start",
|
|
@@ -98,7 +98,9 @@ export class PlaybookRecorder {
|
|
|
98
98
|
try {
|
|
99
99
|
await this.pollAXState();
|
|
100
100
|
}
|
|
101
|
-
catch {
|
|
101
|
+
catch (e) {
|
|
102
|
+
process.stderr.write(`[recorder] AX poll failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
103
|
+
}
|
|
102
104
|
}, AX_POLL_INTERVAL_MS);
|
|
103
105
|
// Start screenshot capture (slower — every 2.5s)
|
|
104
106
|
if (this.captureScreenshots) {
|
|
@@ -108,7 +110,9 @@ export class PlaybookRecorder {
|
|
|
108
110
|
try {
|
|
109
111
|
await this.takeScreenshot();
|
|
110
112
|
}
|
|
111
|
-
catch {
|
|
113
|
+
catch (e) {
|
|
114
|
+
process.stderr.write(`[recorder] screenshot capture failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
115
|
+
}
|
|
112
116
|
}, SCREENSHOT_INTERVAL_MS);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
@@ -123,7 +127,9 @@ export class PlaybookRecorder {
|
|
|
123
127
|
try {
|
|
124
128
|
await this.takeScreenshot();
|
|
125
129
|
}
|
|
126
|
-
catch {
|
|
130
|
+
catch (e) {
|
|
131
|
+
process.stderr.write(`[recorder] final screenshot failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
132
|
+
}
|
|
127
133
|
}
|
|
128
134
|
this.log(`Recording stopped. ${this.events.length} events, ${this.screenshots.length} screenshots captured.`);
|
|
129
135
|
// Convert raw events + screenshots to playbook steps via AI
|
|
@@ -190,7 +196,9 @@ export class PlaybookRecorder {
|
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
|
-
catch {
|
|
199
|
+
catch (e) {
|
|
200
|
+
process.stderr.write(`[recorder] app list poll failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
201
|
+
}
|
|
194
202
|
// 2. Get accessibility tree — find focused element and text field values
|
|
195
203
|
try {
|
|
196
204
|
const tree = await this.runtime.elementTree({ sessionId: this.sessionId, maxDepth: 4 });
|
|
@@ -238,7 +246,9 @@ export class PlaybookRecorder {
|
|
|
238
246
|
this.prevWindowTitle = title;
|
|
239
247
|
}
|
|
240
248
|
}
|
|
241
|
-
catch {
|
|
249
|
+
catch (e) {
|
|
250
|
+
process.stderr.write(`[recorder] window title poll failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
251
|
+
}
|
|
242
252
|
}
|
|
243
253
|
// ── Screenshot Capture ──
|
|
244
254
|
async takeScreenshot() {
|
|
@@ -253,7 +263,9 @@ export class PlaybookRecorder {
|
|
|
253
263
|
this.screenshots.push(record);
|
|
254
264
|
}
|
|
255
265
|
}
|
|
256
|
-
catch {
|
|
266
|
+
catch (e) {
|
|
267
|
+
process.stderr.write(`[recorder] takeScreenshot failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
268
|
+
}
|
|
257
269
|
}
|
|
258
270
|
// ── State Capture ──
|
|
259
271
|
async captureState(label) {
|
|
@@ -271,7 +283,9 @@ export class PlaybookRecorder {
|
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
285
|
}
|
|
274
|
-
catch {
|
|
286
|
+
catch (e) {
|
|
287
|
+
process.stderr.write(`[recorder] initial app state capture failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
288
|
+
}
|
|
275
289
|
// Capture initial tree state
|
|
276
290
|
try {
|
|
277
291
|
const tree = await this.runtime.elementTree({ sessionId: this.sessionId, maxDepth: 4 });
|
|
@@ -281,7 +295,9 @@ export class PlaybookRecorder {
|
|
|
281
295
|
this.prevTextFields = collectTextFields(tree.data);
|
|
282
296
|
}
|
|
283
297
|
}
|
|
284
|
-
catch {
|
|
298
|
+
catch (e) {
|
|
299
|
+
process.stderr.write(`[recorder] initial tree state capture failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
300
|
+
}
|
|
285
301
|
// Take initial screenshot
|
|
286
302
|
if (this.captureScreenshots) {
|
|
287
303
|
await this.takeScreenshot();
|
|
@@ -97,7 +97,9 @@ export class PlaybookRunner {
|
|
|
97
97
|
if (shot.ok)
|
|
98
98
|
screenshotInfo = `Screenshot saved to: ${shot.data.path}`;
|
|
99
99
|
}
|
|
100
|
-
catch {
|
|
100
|
+
catch (e) {
|
|
101
|
+
process.stderr.write(`[playbook-runner] screenshot for recovery failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
102
|
+
}
|
|
101
103
|
// Get current page state
|
|
102
104
|
let pageState = "";
|
|
103
105
|
try {
|
|
@@ -106,7 +108,9 @@ export class PlaybookRunner {
|
|
|
106
108
|
pageState = JSON.stringify(tree.data).slice(0, 3000);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
|
-
catch {
|
|
111
|
+
catch (e) {
|
|
112
|
+
process.stderr.write(`[playbook-runner] elementTree for recovery failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
113
|
+
}
|
|
110
114
|
// Build rich context from playbook metadata
|
|
111
115
|
const playbookContext = buildPlaybookContext(playbook);
|
|
112
116
|
const prompt = `A playbook automation failed. Help me recover.
|
|
@@ -221,7 +225,9 @@ Or if unrecoverable, respond with: { "unrecoverable": true, "reason": "..." }`;
|
|
|
221
225
|
if (tree.ok)
|
|
222
226
|
pageState = JSON.stringify(tree.data).slice(0, 4000);
|
|
223
227
|
}
|
|
224
|
-
catch {
|
|
228
|
+
catch (e) {
|
|
229
|
+
process.stderr.write(`[playbook-runner] elementTree in AI loop failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
230
|
+
}
|
|
225
231
|
const prompt = `Task: ${task}
|
|
226
232
|
|
|
227
233
|
Steps taken so far:
|
|
@@ -63,6 +63,10 @@ export class PlaybookStore {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
/** Reload all playbooks from disk (call after ingestion writes new data). */
|
|
67
|
+
reload() {
|
|
68
|
+
this.load();
|
|
69
|
+
}
|
|
66
70
|
/** Get all loaded playbooks. */
|
|
67
71
|
getAll() {
|
|
68
72
|
return [...this.playbooks.values()];
|
|
@@ -274,6 +278,10 @@ export class PlaybookStore {
|
|
|
274
278
|
if (raw.flows && typeof raw.flows === "object") {
|
|
275
279
|
return this.convertLegacy(raw, filename);
|
|
276
280
|
}
|
|
281
|
+
// Reference format: has selectors but no steps/flows (e.g. *-explore.json from platform_explore)
|
|
282
|
+
if (raw.selectors && typeof raw.selectors === "object") {
|
|
283
|
+
return this.convertLegacy(raw, filename);
|
|
284
|
+
}
|
|
277
285
|
return null;
|
|
278
286
|
}
|
|
279
287
|
/**
|
|
@@ -109,7 +109,9 @@ export class RecoveryEngine {
|
|
|
109
109
|
if (undoStrategy)
|
|
110
110
|
candidates.push(undoStrategy);
|
|
111
111
|
}
|
|
112
|
-
catch {
|
|
112
|
+
catch (e) {
|
|
113
|
+
process.stderr.write(`[recovery] buildUndoStrategy failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
114
|
+
}
|
|
113
115
|
}
|
|
114
116
|
// Reference strategies second (app-specific)
|
|
115
117
|
if (blocker.bundleId) {
|
|
@@ -355,7 +357,9 @@ export class RecoveryEngine {
|
|
|
355
357
|
break;
|
|
356
358
|
}
|
|
357
359
|
}
|
|
358
|
-
catch {
|
|
360
|
+
catch (e) {
|
|
361
|
+
process.stderr.write(`[recovery] malformed reference file: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
362
|
+
}
|
|
359
363
|
}
|
|
360
364
|
}
|
|
361
365
|
catch { /* dir doesn't exist */ }
|
|
@@ -366,7 +370,9 @@ export class RecoveryEngine {
|
|
|
366
370
|
try {
|
|
367
371
|
this.memory.recordError(`recovery:${event.strategyId}`, event.error ?? "", event.success ? event.strategyLabel : null, event.blocker.bundleId ?? undefined);
|
|
368
372
|
}
|
|
369
|
-
catch {
|
|
373
|
+
catch (e) {
|
|
374
|
+
process.stderr.write(`[recovery] recordEvent failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
375
|
+
}
|
|
370
376
|
}
|
|
371
377
|
}
|
|
372
378
|
function sleep(ms) {
|
|
@@ -339,7 +339,9 @@ export class AppMap {
|
|
|
339
339
|
fs.mkdirSync(this.config.mapsDir, { recursive: true });
|
|
340
340
|
writeFileAtomicSync(this.ladderFilePath(bundleId), JSON.stringify(data, null, 2));
|
|
341
341
|
}
|
|
342
|
-
catch {
|
|
342
|
+
catch (e) {
|
|
343
|
+
process.stderr.write(`[app-map] saveGeneratedLadder failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
344
|
+
}
|
|
343
345
|
}
|
|
344
346
|
// ── Create ────────────────────────────────────────────────────────
|
|
345
347
|
createEmpty(bundleId, appName, version = "unknown") {
|
|
@@ -1591,7 +1593,9 @@ export class AppMap {
|
|
|
1591
1593
|
writeFileAtomicSync(this.filePath(data.app), JSON.stringify(data, null, 2) + "\n");
|
|
1592
1594
|
this.dirty.delete(data.app); // Only remove the one we just wrote
|
|
1593
1595
|
}
|
|
1594
|
-
catch {
|
|
1596
|
+
catch (e) {
|
|
1597
|
+
process.stderr.write(`[app-map] urgent mastery save failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
1598
|
+
}
|
|
1595
1599
|
}
|
|
1596
1600
|
}
|
|
1597
1601
|
refreshMastery(bundleId) {
|