screenhand 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /package/{playbooks → dist-references}/devpost.json +0 -0
@@ -0,0 +1,408 @@
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
+ * Playbook Runner — the brain
19
+ *
20
+ * 1. Match task → playbook
21
+ * 2. Execute playbook steps (fast, no AI)
22
+ * 3. If step fails → ask AI to recover
23
+ * 4. Save AI's recovery steps back into playbook
24
+ * 5. Loop forever in monitor mode
25
+ */
26
+ import Anthropic from "@anthropic-ai/sdk";
27
+ import { PlaybookEngine } from "./engine.js";
28
+ import { PlaybookStore } from "./store.js";
29
+ export class PlaybookRunner {
30
+ runtime;
31
+ engine;
32
+ store;
33
+ ai;
34
+ model;
35
+ maxRecovery;
36
+ log;
37
+ constructor(runtime, playbookDir, options = {}) {
38
+ this.runtime = runtime;
39
+ this.engine = new PlaybookEngine(runtime);
40
+ this.store = new PlaybookStore(playbookDir);
41
+ this.store.load();
42
+ this.ai = new Anthropic();
43
+ this.model = options.model ?? "claude-sonnet-4-20250514";
44
+ this.maxRecovery = options.maxRecoveryAttempts ?? 3;
45
+ this.log = options.onLog ?? ((msg) => console.error(`[PlaybookRunner] ${msg}`));
46
+ }
47
+ /**
48
+ * Execute a task. Tries playbook first, falls back to AI.
49
+ */
50
+ async execute(sessionId, task) {
51
+ // 1. Find matching playbook
52
+ const playbook = this.store.matchByTask(task);
53
+ if (playbook && playbook.steps.length > 0) {
54
+ this.log(`Found playbook: ${playbook.name} (${playbook.successCount} successes)`);
55
+ // 2. Run playbook
56
+ const result = await this.engine.run(sessionId, playbook, {
57
+ onStep: (i, step, res) => {
58
+ this.log(` Step ${i + 1}/${playbook.steps.length}: ${step.description ?? step.action} → ${res}`);
59
+ },
60
+ });
61
+ if (result.success) {
62
+ this.store.recordOutcome(playbook.id, true);
63
+ this.log(`Playbook completed successfully in ${result.durationMs}ms`);
64
+ return result;
65
+ }
66
+ // 3. Playbook failed at a step — try AI recovery
67
+ this.log(`Playbook failed at step ${result.failedAtStep}: ${result.error}`);
68
+ const recovery = await this.aiRecover(sessionId, playbook, result);
69
+ if (recovery) {
70
+ this.store.recordOutcome(playbook.id, true);
71
+ return { ...result, success: true, aiRecovery: recovery };
72
+ }
73
+ this.store.recordOutcome(playbook.id, false);
74
+ return result;
75
+ }
76
+ // Playbook found but has no executable steps (legacy format with flows/selectors)
77
+ // → Use AI mode but feed it the playbook's rich metadata as context
78
+ if (playbook) {
79
+ this.log(`Found reference playbook: ${playbook.name} (flows/selectors, no executable steps)`);
80
+ return this.aiExecute(sessionId, task, playbook);
81
+ }
82
+ // No playbook found — pure AI mode
83
+ this.log(`No playbook found for: "${task}". Using AI.`);
84
+ return this.aiExecute(sessionId, task);
85
+ }
86
+ /**
87
+ * AI recovery — when a playbook step fails, ask AI to fix it.
88
+ */
89
+ async aiRecover(sessionId, playbook, failResult) {
90
+ const failedStep = playbook.steps[failResult.failedAtStep];
91
+ if (!failedStep)
92
+ return null;
93
+ // Take screenshot for context
94
+ let screenshotInfo = "";
95
+ try {
96
+ const shot = await this.runtime.screenshot({ sessionId });
97
+ if (shot.ok)
98
+ screenshotInfo = `Screenshot saved to: ${shot.data.path}`;
99
+ }
100
+ catch { /* ignore */ }
101
+ // Get current page state
102
+ let pageState = "";
103
+ try {
104
+ const tree = await this.runtime.elementTree({ sessionId, maxDepth: 4 });
105
+ if (tree.ok) {
106
+ pageState = JSON.stringify(tree.data).slice(0, 3000);
107
+ }
108
+ }
109
+ catch { /* ignore */ }
110
+ // Build rich context from playbook metadata
111
+ const playbookContext = buildPlaybookContext(playbook);
112
+ const prompt = `A playbook automation failed. Help me recover.
113
+
114
+ Playbook: ${playbook.name}
115
+ Platform: ${playbook.platform}
116
+ Failed at step ${failResult.failedAtStep + 1}/${playbook.steps.length}:
117
+ Action: ${failedStep.action}
118
+ Target: ${JSON.stringify(failedStep.target)}
119
+ Error: ${failResult.error}
120
+
121
+ Steps completed before failure:
122
+ ${playbook.steps.slice(0, failResult.failedAtStep).map((s, i) => ` ${i + 1}. ${s.description ?? s.action}`).join("\n")}
123
+
124
+ Remaining steps after failure:
125
+ ${playbook.steps.slice(failResult.failedAtStep).map((s, i) => ` ${failResult.failedAtStep + i + 1}. ${s.description ?? s.action}`).join("\n")}
126
+
127
+ ${playbookContext}
128
+
129
+ Current UI state (accessibility tree):
130
+ ${pageState}
131
+
132
+ ${screenshotInfo}
133
+
134
+ What should I do to recover? Respond with a JSON array of recovery steps:
135
+ [
136
+ { "action": "press", "target": "...", "description": "..." },
137
+ { "action": "wait", "ms": 1000 }
138
+ ]
139
+
140
+ Or if unrecoverable, respond with: { "unrecoverable": true, "reason": "..." }`;
141
+ for (let attempt = 0; attempt < this.maxRecovery; attempt++) {
142
+ try {
143
+ const resp = await this.ai.messages.create({
144
+ model: this.model,
145
+ max_tokens: 1024,
146
+ messages: [{ role: "user", content: prompt }],
147
+ });
148
+ const text = resp.content[0]?.type === "text" ? resp.content[0].text : "";
149
+ const jsonMatch = text.match(/\[[\s\S]*\]|\{[\s\S]*\}/);
150
+ if (!jsonMatch)
151
+ continue;
152
+ const parsed = JSON.parse(jsonMatch[0]);
153
+ // Check if unrecoverable
154
+ if (parsed.unrecoverable) {
155
+ this.log(`AI says unrecoverable: ${parsed.reason}`);
156
+ return null;
157
+ }
158
+ // Execute recovery steps
159
+ const recoverySteps = Array.isArray(parsed) ? parsed : [parsed];
160
+ this.log(`AI suggests ${recoverySteps.length} recovery steps`);
161
+ for (const step of recoverySteps) {
162
+ const stepResult = await this.engine.run(sessionId, {
163
+ id: "recovery",
164
+ name: "AI Recovery",
165
+ description: "",
166
+ platform: playbook.platform,
167
+ steps: [step],
168
+ version: "0",
169
+ tags: [],
170
+ successCount: 0,
171
+ failCount: 0,
172
+ });
173
+ if (!stepResult.success) {
174
+ this.log(`Recovery step failed: ${stepResult.error}`);
175
+ continue;
176
+ }
177
+ }
178
+ // Now try remaining playbook steps
179
+ const remaining = {
180
+ ...playbook,
181
+ id: `${playbook.id}_remaining`,
182
+ steps: playbook.steps.slice(failResult.failedAtStep + 1),
183
+ };
184
+ if (remaining.steps.length > 0) {
185
+ const remainingResult = await this.engine.run(sessionId, remaining, {
186
+ onStep: (i, step, res) => {
187
+ const globalIdx = failResult.failedAtStep + 1 + i;
188
+ this.log(` Step ${globalIdx + 1}/${playbook.steps.length}: ${step.description ?? step.action} → ${res}`);
189
+ },
190
+ });
191
+ if (!remainingResult.success) {
192
+ this.log(`Remaining steps failed at ${remainingResult.failedAtStep}`);
193
+ return null;
194
+ }
195
+ }
196
+ // Save recovery steps back into playbook for next time
197
+ this.patchPlaybook(playbook, failResult.failedAtStep, recoverySteps);
198
+ return `AI recovered with ${recoverySteps.length} steps, then completed remaining ${remaining.steps.length} steps`;
199
+ }
200
+ catch (err) {
201
+ this.log(`AI recovery attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
202
+ }
203
+ }
204
+ return null;
205
+ }
206
+ /**
207
+ * AI execution — optionally guided by a reference playbook's metadata.
208
+ * When a playbook has selectors/flows/errors but no executable steps,
209
+ * AI uses that knowledge to make smarter decisions.
210
+ * After success, saves the steps as a new playbook.
211
+ */
212
+ async aiExecute(sessionId, task, refPlaybook) {
213
+ const start = Date.now();
214
+ const executedSteps = [];
215
+ const playbookContext = refPlaybook ? buildPlaybookContext(refPlaybook) : "";
216
+ // Simple AI loop — observe, decide, act
217
+ for (let i = 0; i < 20; i++) {
218
+ let pageState = "";
219
+ try {
220
+ const tree = await this.runtime.elementTree({ sessionId, maxDepth: 4 });
221
+ if (tree.ok)
222
+ pageState = JSON.stringify(tree.data).slice(0, 4000);
223
+ }
224
+ catch { /* ignore */ }
225
+ const prompt = `Task: ${task}
226
+
227
+ Steps taken so far:
228
+ ${executedSteps.map((s, idx) => `${idx + 1}. ${s.description ?? s.action}`).join("\n") || "(none)"}
229
+
230
+ Current UI state:
231
+ ${pageState}
232
+ ${playbookContext ? `\n--- PLAYBOOK REFERENCE ---\n${playbookContext}\nUse the selectors, flows, and error solutions above to guide your actions. Prefer data-testid selectors over text matching.\n---\n` : ""}
233
+ What's the next step? Respond with ONE step as JSON:
234
+ { "action": "press|type_into|navigate|key|key_combo|menu_click|scroll|wait", "target": "...", "text": "...", "url": "...", "keys": [...], "menuPath": ["File", "Save"], "ms": 1000, "description": "..." }
235
+
236
+ Or if done: { "action": "done", "description": "Task complete" }`;
237
+ try {
238
+ const resp = await this.ai.messages.create({
239
+ model: this.model,
240
+ max_tokens: 512,
241
+ messages: [{ role: "user", content: prompt }],
242
+ });
243
+ const text = resp.content[0]?.type === "text" ? resp.content[0].text : "";
244
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
245
+ if (!jsonMatch)
246
+ continue;
247
+ const step = JSON.parse(jsonMatch[0]);
248
+ if (step.action === "done") {
249
+ // Save as new playbook for next time
250
+ if (executedSteps.length > 0) {
251
+ this.saveNewPlaybook(task, executedSteps);
252
+ }
253
+ return {
254
+ playbook: "ai_generated",
255
+ success: true,
256
+ stepsCompleted: executedSteps.length,
257
+ totalSteps: executedSteps.length,
258
+ failedAtStep: -1,
259
+ durationMs: Date.now() - start,
260
+ };
261
+ }
262
+ // Execute the step
263
+ const stepResult = await this.engine.run(sessionId, {
264
+ id: "ai_step",
265
+ name: "AI Step",
266
+ description: "",
267
+ platform: "unknown",
268
+ steps: [step],
269
+ version: "0",
270
+ tags: [],
271
+ successCount: 0,
272
+ failCount: 0,
273
+ });
274
+ if (stepResult.success) {
275
+ executedSteps.push(step);
276
+ this.log(`AI step ${executedSteps.length}: ${step.description ?? step.action}`);
277
+ }
278
+ else {
279
+ this.log(`AI step failed: ${stepResult.error}`);
280
+ }
281
+ await sleep(300);
282
+ }
283
+ catch (err) {
284
+ this.log(`AI step error: ${err instanceof Error ? err.message : String(err)}`);
285
+ }
286
+ }
287
+ return {
288
+ playbook: "ai_generated",
289
+ success: false,
290
+ stepsCompleted: executedSteps.length,
291
+ totalSteps: -1,
292
+ failedAtStep: executedSteps.length,
293
+ error: "Max AI steps reached",
294
+ durationMs: Date.now() - start,
295
+ };
296
+ }
297
+ /**
298
+ * Patch a playbook — insert recovery steps at the failure point.
299
+ */
300
+ patchPlaybook(playbook, failedAt, recoverySteps) {
301
+ const patched = {
302
+ ...playbook,
303
+ steps: [
304
+ ...playbook.steps.slice(0, failedAt),
305
+ ...recoverySteps,
306
+ ...playbook.steps.slice(failedAt),
307
+ ],
308
+ version: bumpVersion(playbook.version),
309
+ };
310
+ this.store.save(patched);
311
+ this.log(`Patched playbook ${playbook.id}: inserted ${recoverySteps.length} recovery steps at position ${failedAt}`);
312
+ }
313
+ /**
314
+ * Save AI-generated steps as a new playbook.
315
+ */
316
+ saveNewPlaybook(task, steps) {
317
+ const id = `auto_${Date.now()}`;
318
+ const playbook = {
319
+ id,
320
+ name: task.slice(0, 80),
321
+ description: `Auto-generated from AI execution: ${task}`,
322
+ platform: "unknown",
323
+ steps,
324
+ version: "1.0.0",
325
+ tags: task.toLowerCase().split(/\W+/).filter((w) => w.length >= 3),
326
+ successCount: 1,
327
+ failCount: 0,
328
+ lastRun: new Date().toISOString(),
329
+ };
330
+ this.store.save(playbook);
331
+ this.log(`Saved new playbook: ${id} (${steps.length} steps)`);
332
+ }
333
+ /** Get all loaded playbooks. */
334
+ listPlaybooks() {
335
+ return this.store.getAll();
336
+ }
337
+ /** Reload playbooks from disk. */
338
+ reload() {
339
+ this.store.load();
340
+ }
341
+ }
342
+ function sleep(ms) {
343
+ return new Promise((resolve) => setTimeout(resolve, ms));
344
+ }
345
+ function bumpVersion(version) {
346
+ const parts = version.split(".").map(Number);
347
+ if (parts.length === 3) {
348
+ parts[2]++;
349
+ return parts.join(".");
350
+ }
351
+ return version + ".1";
352
+ }
353
+ /**
354
+ * Build a context string from playbook metadata for AI consumption.
355
+ * Includes selectors, flows, known errors, detection expressions.
356
+ */
357
+ function buildPlaybookContext(playbook) {
358
+ const parts = [];
359
+ if (playbook.urls && Object.keys(playbook.urls).length > 0) {
360
+ parts.push("URLS:\n" + Object.entries(playbook.urls).map(([k, v]) => ` ${k}: ${v}`).join("\n"));
361
+ }
362
+ if (playbook.selectors && Object.keys(playbook.selectors).length > 0) {
363
+ parts.push("SELECTORS:");
364
+ for (const [group, sels] of Object.entries(playbook.selectors)) {
365
+ parts.push(` [${group}]`);
366
+ for (const [name, sel] of Object.entries(sels)) {
367
+ parts.push(` ${name}: ${sel}`);
368
+ }
369
+ }
370
+ }
371
+ if (playbook.flows && Object.keys(playbook.flows).length > 0) {
372
+ parts.push("FLOWS:");
373
+ for (const [name, flow] of Object.entries(playbook.flows)) {
374
+ parts.push(` [${name}]`);
375
+ for (const step of flow.steps) {
376
+ parts.push(` - ${step}`);
377
+ }
378
+ if (flow.guards) {
379
+ parts.push(` Guards:`);
380
+ for (const g of flow.guards)
381
+ parts.push(` ! ${g}`);
382
+ }
383
+ }
384
+ }
385
+ if (playbook.detection && Object.keys(playbook.detection).length > 0) {
386
+ parts.push("DETECTION (JS expressions):");
387
+ for (const [name, expr] of Object.entries(playbook.detection)) {
388
+ parts.push(` ${name}: ${expr}`);
389
+ }
390
+ }
391
+ if (playbook.errors && playbook.errors.length > 0) {
392
+ parts.push("KNOWN ERRORS & SOLUTIONS:");
393
+ for (const e of playbook.errors) {
394
+ parts.push(` [${e.severity}] ${e.error}`);
395
+ parts.push(` Context: ${e.context}`);
396
+ parts.push(` Solution: ${e.solution}`);
397
+ }
398
+ }
399
+ if (playbook.policyNotes && Object.keys(playbook.policyNotes).length > 0) {
400
+ parts.push("POLICY NOTES:");
401
+ for (const [cat, notes] of Object.entries(playbook.policyNotes)) {
402
+ parts.push(` [${cat}]`);
403
+ for (const n of notes)
404
+ parts.push(` - ${n}`);
405
+ }
406
+ }
407
+ return parts.join("\n");
408
+ }