screenhand 0.1.1 → 0.2.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 (177) hide show
  1. package/README.md +458 -93
  2. package/dist/.audit-log.jsonl +55 -0
  3. package/dist/.screenhand/memory/.lock +1 -0
  4. package/dist/.screenhand/memory/actions.jsonl +85 -0
  5. package/dist/.screenhand/memory/errors.jsonl +5 -0
  6. package/dist/.screenhand/memory/errors.jsonl.bak +4 -0
  7. package/dist/.screenhand/memory/state.json +35 -0
  8. package/dist/.screenhand/memory/state.json.bak +35 -0
  9. package/dist/.screenhand/memory/strategies.jsonl +12 -0
  10. package/dist/agent/cli.js +73 -0
  11. package/dist/agent/loop.js +258 -0
  12. package/dist/config.js +9 -0
  13. package/dist/index.js +56 -0
  14. package/dist/logging/timeline-logger.js +29 -0
  15. package/dist/mcp/mcp-stdio-server.js +448 -0
  16. package/dist/mcp/server.js +347 -0
  17. package/dist/mcp-desktop.js +2731 -0
  18. package/dist/mcp-entry.js +59 -0
  19. package/dist/memory/recall.js +160 -0
  20. package/dist/memory/research.js +98 -0
  21. package/dist/memory/seeds.js +89 -0
  22. package/dist/memory/session.js +161 -0
  23. package/dist/memory/store.js +391 -0
  24. package/dist/memory/types.js +4 -0
  25. package/dist/monitor/codex-monitor.js +377 -0
  26. package/dist/monitor/task-queue.js +84 -0
  27. package/dist/monitor/types.js +49 -0
  28. package/dist/native/bridge-client.js +174 -0
  29. package/dist/native/macos-bridge-client.js +5 -0
  30. package/dist/npm-publish-helper.js +117 -0
  31. package/dist/npm-token-cdp.js +113 -0
  32. package/dist/npm-token-create.js +135 -0
  33. package/dist/npm-token-finish.js +126 -0
  34. package/dist/playbook/engine.js +193 -0
  35. package/dist/playbook/index.js +4 -0
  36. package/dist/playbook/recorder.js +519 -0
  37. package/dist/playbook/runner.js +392 -0
  38. package/dist/playbook/store.js +166 -0
  39. package/dist/playbook/types.js +4 -0
  40. package/dist/runtime/accessibility-adapter.js +377 -0
  41. package/dist/runtime/app-adapter.js +48 -0
  42. package/dist/runtime/applescript-adapter.js +283 -0
  43. package/dist/runtime/ax-role-map.js +80 -0
  44. package/dist/runtime/browser-adapter.js +36 -0
  45. package/dist/runtime/cdp-chrome-adapter.js +505 -0
  46. package/dist/runtime/composite-adapter.js +205 -0
  47. package/dist/runtime/executor.js +250 -0
  48. package/dist/runtime/locator-cache.js +12 -0
  49. package/dist/runtime/planning-loop.js +47 -0
  50. package/dist/runtime/service.js +372 -0
  51. package/dist/runtime/session-manager.js +28 -0
  52. package/dist/runtime/state-observer.js +105 -0
  53. package/dist/runtime/vision-adapter.js +208 -0
  54. package/dist/scripts/codex-monitor-daemon.js +335 -0
  55. package/dist/scripts/supervisor-daemon.js +272 -0
  56. package/dist/scripts/worker-daemon.js +228 -0
  57. package/dist/src/agent/cli.js +82 -0
  58. package/dist/src/agent/loop.js +274 -0
  59. package/{src/config.ts → dist/src/config.js} +5 -10
  60. package/{src/index.ts → dist/src/index.js} +32 -52
  61. package/dist/src/jobs/manager.js +237 -0
  62. package/dist/src/jobs/runner.js +683 -0
  63. package/dist/src/jobs/store.js +102 -0
  64. package/dist/src/jobs/types.js +30 -0
  65. package/dist/src/jobs/worker.js +97 -0
  66. package/dist/src/logging/timeline-logger.js +45 -0
  67. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  68. package/dist/src/mcp/server.js +363 -0
  69. package/dist/src/mcp-entry.js +60 -0
  70. package/dist/src/memory/recall.js +170 -0
  71. package/dist/src/memory/research.js +104 -0
  72. package/dist/src/memory/seeds.js +101 -0
  73. package/dist/src/memory/service.js +421 -0
  74. package/dist/src/memory/session.js +169 -0
  75. package/dist/src/memory/store.js +422 -0
  76. package/dist/src/memory/types.js +17 -0
  77. package/dist/src/monitor/codex-monitor.js +382 -0
  78. package/dist/src/monitor/task-queue.js +97 -0
  79. package/dist/src/monitor/types.js +62 -0
  80. package/dist/src/native/bridge-client.js +190 -0
  81. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  82. package/dist/src/playbook/engine.js +201 -0
  83. package/dist/src/playbook/index.js +20 -0
  84. package/dist/src/playbook/recorder.js +535 -0
  85. package/dist/src/playbook/runner.js +408 -0
  86. package/dist/src/playbook/store.js +183 -0
  87. package/dist/src/playbook/types.js +17 -0
  88. package/dist/src/runtime/accessibility-adapter.js +393 -0
  89. package/dist/src/runtime/app-adapter.js +64 -0
  90. package/dist/src/runtime/applescript-adapter.js +299 -0
  91. package/dist/src/runtime/ax-role-map.js +96 -0
  92. package/dist/src/runtime/browser-adapter.js +52 -0
  93. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  94. package/dist/src/runtime/composite-adapter.js +221 -0
  95. package/dist/src/runtime/execution-contract.js +159 -0
  96. package/dist/src/runtime/executor.js +266 -0
  97. package/{src/runtime/locator-cache.ts → dist/src/runtime/locator-cache.js} +10 -15
  98. package/dist/src/runtime/planning-loop.js +63 -0
  99. package/dist/src/runtime/service.js +388 -0
  100. package/dist/src/runtime/session-manager.js +60 -0
  101. package/dist/src/runtime/state-observer.js +121 -0
  102. package/dist/src/runtime/vision-adapter.js +224 -0
  103. package/dist/src/supervisor/locks.js +186 -0
  104. package/dist/src/supervisor/supervisor.js +403 -0
  105. package/dist/src/supervisor/types.js +30 -0
  106. package/dist/src/test-mcp-protocol.js +154 -0
  107. package/dist/src/types.js +17 -0
  108. package/dist/src/util/atomic-write.js +118 -0
  109. package/dist/test-mcp-protocol.js +138 -0
  110. package/dist/types.js +1 -0
  111. package/package.json +18 -4
  112. package/.claude/commands/automate.md +0 -28
  113. package/.claude/commands/debug-ui.md +0 -19
  114. package/.claude/commands/screenshot.md +0 -15
  115. package/.github/FUNDING.yml +0 -1
  116. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  117. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  118. package/.mcp.json +0 -8
  119. package/DESKTOP_MCP_GUIDE.md +0 -92
  120. package/SECURITY.md +0 -44
  121. package/docs/architecture.md +0 -47
  122. package/install-skills.sh +0 -19
  123. package/mcp-bridge.ts +0 -271
  124. package/mcp-desktop.ts +0 -1221
  125. package/native/macos-bridge/Package.swift +0 -21
  126. package/native/macos-bridge/Sources/AccessibilityBridge.swift +0 -261
  127. package/native/macos-bridge/Sources/AppManagement.swift +0 -129
  128. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +0 -242
  129. package/native/macos-bridge/Sources/ObserverBridge.swift +0 -120
  130. package/native/macos-bridge/Sources/VisionBridge.swift +0 -80
  131. package/native/macos-bridge/Sources/main.swift +0 -345
  132. package/native/windows-bridge/AppManagement.cs +0 -234
  133. package/native/windows-bridge/InputBridge.cs +0 -436
  134. package/native/windows-bridge/Program.cs +0 -265
  135. package/native/windows-bridge/ScreenCapture.cs +0 -329
  136. package/native/windows-bridge/UIAutomationBridge.cs +0 -571
  137. package/native/windows-bridge/WindowsBridge.csproj +0 -17
  138. package/playbooks/devpost.json +0 -186
  139. package/playbooks/instagram.json +0 -41
  140. package/playbooks/instagram_v2.json +0 -201
  141. package/playbooks/x_v1.json +0 -211
  142. package/scripts/devpost-live-loop.mjs +0 -421
  143. package/src/logging/timeline-logger.ts +0 -55
  144. package/src/mcp/server.ts +0 -449
  145. package/src/memory/recall.ts +0 -191
  146. package/src/memory/research.ts +0 -146
  147. package/src/memory/seeds.ts +0 -123
  148. package/src/memory/session.ts +0 -201
  149. package/src/memory/store.ts +0 -434
  150. package/src/memory/types.ts +0 -69
  151. package/src/native/bridge-client.ts +0 -239
  152. package/src/runtime/accessibility-adapter.ts +0 -487
  153. package/src/runtime/app-adapter.ts +0 -169
  154. package/src/runtime/applescript-adapter.ts +0 -376
  155. package/src/runtime/ax-role-map.ts +0 -102
  156. package/src/runtime/browser-adapter.ts +0 -129
  157. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  158. package/src/runtime/composite-adapter.ts +0 -274
  159. package/src/runtime/executor.ts +0 -396
  160. package/src/runtime/planning-loop.ts +0 -81
  161. package/src/runtime/service.ts +0 -448
  162. package/src/runtime/session-manager.ts +0 -50
  163. package/src/runtime/state-observer.ts +0 -136
  164. package/src/runtime/vision-adapter.ts +0 -297
  165. package/src/types.ts +0 -297
  166. package/tests/bridge-client.test.ts +0 -176
  167. package/tests/browser-stealth.test.ts +0 -210
  168. package/tests/composite-adapter.test.ts +0 -64
  169. package/tests/mcp-server.test.ts +0 -151
  170. package/tests/memory-recall.test.ts +0 -339
  171. package/tests/memory-research.test.ts +0 -159
  172. package/tests/memory-seeds.test.ts +0 -120
  173. package/tests/memory-store.test.ts +0 -392
  174. package/tests/types.test.ts +0 -92
  175. package/tsconfig.check.json +0 -17
  176. package/tsconfig.json +0 -19
  177. package/vitest.config.ts +0 -8
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Playbook Runner — the brain
3
+ *
4
+ * 1. Match task → playbook
5
+ * 2. Execute playbook steps (fast, no AI)
6
+ * 3. If step fails → ask AI to recover
7
+ * 4. Save AI's recovery steps back into playbook
8
+ * 5. Loop forever in monitor mode
9
+ */
10
+ import Anthropic from "@anthropic-ai/sdk";
11
+ import { PlaybookEngine } from "./engine.js";
12
+ import { PlaybookStore } from "./store.js";
13
+ export class PlaybookRunner {
14
+ runtime;
15
+ engine;
16
+ store;
17
+ ai;
18
+ model;
19
+ maxRecovery;
20
+ log;
21
+ constructor(runtime, playbookDir, options = {}) {
22
+ this.runtime = runtime;
23
+ this.engine = new PlaybookEngine(runtime);
24
+ this.store = new PlaybookStore(playbookDir);
25
+ this.store.load();
26
+ this.ai = new Anthropic();
27
+ this.model = options.model ?? "claude-sonnet-4-20250514";
28
+ this.maxRecovery = options.maxRecoveryAttempts ?? 3;
29
+ this.log = options.onLog ?? ((msg) => console.error(`[PlaybookRunner] ${msg}`));
30
+ }
31
+ /**
32
+ * Execute a task. Tries playbook first, falls back to AI.
33
+ */
34
+ async execute(sessionId, task) {
35
+ // 1. Find matching playbook
36
+ const playbook = this.store.matchByTask(task);
37
+ if (playbook && playbook.steps.length > 0) {
38
+ this.log(`Found playbook: ${playbook.name} (${playbook.successCount} successes)`);
39
+ // 2. Run playbook
40
+ const result = await this.engine.run(sessionId, playbook, {
41
+ onStep: (i, step, res) => {
42
+ this.log(` Step ${i + 1}/${playbook.steps.length}: ${step.description ?? step.action} → ${res}`);
43
+ },
44
+ });
45
+ if (result.success) {
46
+ this.store.recordOutcome(playbook.id, true);
47
+ this.log(`Playbook completed successfully in ${result.durationMs}ms`);
48
+ return result;
49
+ }
50
+ // 3. Playbook failed at a step — try AI recovery
51
+ this.log(`Playbook failed at step ${result.failedAtStep}: ${result.error}`);
52
+ const recovery = await this.aiRecover(sessionId, playbook, result);
53
+ if (recovery) {
54
+ this.store.recordOutcome(playbook.id, true);
55
+ return { ...result, success: true, aiRecovery: recovery };
56
+ }
57
+ this.store.recordOutcome(playbook.id, false);
58
+ return result;
59
+ }
60
+ // Playbook found but has no executable steps (legacy format with flows/selectors)
61
+ // → Use AI mode but feed it the playbook's rich metadata as context
62
+ if (playbook) {
63
+ this.log(`Found reference playbook: ${playbook.name} (flows/selectors, no executable steps)`);
64
+ return this.aiExecute(sessionId, task, playbook);
65
+ }
66
+ // No playbook found — pure AI mode
67
+ this.log(`No playbook found for: "${task}". Using AI.`);
68
+ return this.aiExecute(sessionId, task);
69
+ }
70
+ /**
71
+ * AI recovery — when a playbook step fails, ask AI to fix it.
72
+ */
73
+ async aiRecover(sessionId, playbook, failResult) {
74
+ const failedStep = playbook.steps[failResult.failedAtStep];
75
+ if (!failedStep)
76
+ return null;
77
+ // Take screenshot for context
78
+ let screenshotInfo = "";
79
+ try {
80
+ const shot = await this.runtime.screenshot({ sessionId });
81
+ if (shot.ok)
82
+ screenshotInfo = `Screenshot saved to: ${shot.data.path}`;
83
+ }
84
+ catch { /* ignore */ }
85
+ // Get current page state
86
+ let pageState = "";
87
+ try {
88
+ const tree = await this.runtime.elementTree({ sessionId, maxDepth: 4 });
89
+ if (tree.ok) {
90
+ pageState = JSON.stringify(tree.data).slice(0, 3000);
91
+ }
92
+ }
93
+ catch { /* ignore */ }
94
+ // Build rich context from playbook metadata
95
+ const playbookContext = buildPlaybookContext(playbook);
96
+ const prompt = `A playbook automation failed. Help me recover.
97
+
98
+ Playbook: ${playbook.name}
99
+ Platform: ${playbook.platform}
100
+ Failed at step ${failResult.failedAtStep + 1}/${playbook.steps.length}:
101
+ Action: ${failedStep.action}
102
+ Target: ${JSON.stringify(failedStep.target)}
103
+ Error: ${failResult.error}
104
+
105
+ Steps completed before failure:
106
+ ${playbook.steps.slice(0, failResult.failedAtStep).map((s, i) => ` ${i + 1}. ${s.description ?? s.action}`).join("\n")}
107
+
108
+ Remaining steps after failure:
109
+ ${playbook.steps.slice(failResult.failedAtStep).map((s, i) => ` ${failResult.failedAtStep + i + 1}. ${s.description ?? s.action}`).join("\n")}
110
+
111
+ ${playbookContext}
112
+
113
+ Current UI state (accessibility tree):
114
+ ${pageState}
115
+
116
+ ${screenshotInfo}
117
+
118
+ What should I do to recover? Respond with a JSON array of recovery steps:
119
+ [
120
+ { "action": "press", "target": "...", "description": "..." },
121
+ { "action": "wait", "ms": 1000 }
122
+ ]
123
+
124
+ Or if unrecoverable, respond with: { "unrecoverable": true, "reason": "..." }`;
125
+ for (let attempt = 0; attempt < this.maxRecovery; attempt++) {
126
+ try {
127
+ const resp = await this.ai.messages.create({
128
+ model: this.model,
129
+ max_tokens: 1024,
130
+ messages: [{ role: "user", content: prompt }],
131
+ });
132
+ const text = resp.content[0]?.type === "text" ? resp.content[0].text : "";
133
+ const jsonMatch = text.match(/\[[\s\S]*\]|\{[\s\S]*\}/);
134
+ if (!jsonMatch)
135
+ continue;
136
+ const parsed = JSON.parse(jsonMatch[0]);
137
+ // Check if unrecoverable
138
+ if (parsed.unrecoverable) {
139
+ this.log(`AI says unrecoverable: ${parsed.reason}`);
140
+ return null;
141
+ }
142
+ // Execute recovery steps
143
+ const recoverySteps = Array.isArray(parsed) ? parsed : [parsed];
144
+ this.log(`AI suggests ${recoverySteps.length} recovery steps`);
145
+ for (const step of recoverySteps) {
146
+ const stepResult = await this.engine.run(sessionId, {
147
+ id: "recovery",
148
+ name: "AI Recovery",
149
+ description: "",
150
+ platform: playbook.platform,
151
+ steps: [step],
152
+ version: "0",
153
+ tags: [],
154
+ successCount: 0,
155
+ failCount: 0,
156
+ });
157
+ if (!stepResult.success) {
158
+ this.log(`Recovery step failed: ${stepResult.error}`);
159
+ continue;
160
+ }
161
+ }
162
+ // Now try remaining playbook steps
163
+ const remaining = {
164
+ ...playbook,
165
+ id: `${playbook.id}_remaining`,
166
+ steps: playbook.steps.slice(failResult.failedAtStep + 1),
167
+ };
168
+ if (remaining.steps.length > 0) {
169
+ const remainingResult = await this.engine.run(sessionId, remaining, {
170
+ onStep: (i, step, res) => {
171
+ const globalIdx = failResult.failedAtStep + 1 + i;
172
+ this.log(` Step ${globalIdx + 1}/${playbook.steps.length}: ${step.description ?? step.action} → ${res}`);
173
+ },
174
+ });
175
+ if (!remainingResult.success) {
176
+ this.log(`Remaining steps failed at ${remainingResult.failedAtStep}`);
177
+ return null;
178
+ }
179
+ }
180
+ // Save recovery steps back into playbook for next time
181
+ this.patchPlaybook(playbook, failResult.failedAtStep, recoverySteps);
182
+ return `AI recovered with ${recoverySteps.length} steps, then completed remaining ${remaining.steps.length} steps`;
183
+ }
184
+ catch (err) {
185
+ this.log(`AI recovery attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`);
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ /**
191
+ * AI execution — optionally guided by a reference playbook's metadata.
192
+ * When a playbook has selectors/flows/errors but no executable steps,
193
+ * AI uses that knowledge to make smarter decisions.
194
+ * After success, saves the steps as a new playbook.
195
+ */
196
+ async aiExecute(sessionId, task, refPlaybook) {
197
+ const start = Date.now();
198
+ const executedSteps = [];
199
+ const playbookContext = refPlaybook ? buildPlaybookContext(refPlaybook) : "";
200
+ // Simple AI loop — observe, decide, act
201
+ for (let i = 0; i < 20; i++) {
202
+ let pageState = "";
203
+ try {
204
+ const tree = await this.runtime.elementTree({ sessionId, maxDepth: 4 });
205
+ if (tree.ok)
206
+ pageState = JSON.stringify(tree.data).slice(0, 4000);
207
+ }
208
+ catch { /* ignore */ }
209
+ const prompt = `Task: ${task}
210
+
211
+ Steps taken so far:
212
+ ${executedSteps.map((s, idx) => `${idx + 1}. ${s.description ?? s.action}`).join("\n") || "(none)"}
213
+
214
+ Current UI state:
215
+ ${pageState}
216
+ ${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` : ""}
217
+ What's the next step? Respond with ONE step as JSON:
218
+ { "action": "press|type_into|navigate|key_combo|scroll|wait", "target": "...", "text": "...", "url": "...", "keys": [...], "ms": 1000, "description": "..." }
219
+
220
+ Or if done: { "action": "done", "description": "Task complete" }`;
221
+ try {
222
+ const resp = await this.ai.messages.create({
223
+ model: this.model,
224
+ max_tokens: 512,
225
+ messages: [{ role: "user", content: prompt }],
226
+ });
227
+ const text = resp.content[0]?.type === "text" ? resp.content[0].text : "";
228
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
229
+ if (!jsonMatch)
230
+ continue;
231
+ const step = JSON.parse(jsonMatch[0]);
232
+ if (step.action === "done") {
233
+ // Save as new playbook for next time
234
+ if (executedSteps.length > 0) {
235
+ this.saveNewPlaybook(task, executedSteps);
236
+ }
237
+ return {
238
+ playbook: "ai_generated",
239
+ success: true,
240
+ stepsCompleted: executedSteps.length,
241
+ totalSteps: executedSteps.length,
242
+ failedAtStep: -1,
243
+ durationMs: Date.now() - start,
244
+ };
245
+ }
246
+ // Execute the step
247
+ const stepResult = await this.engine.run(sessionId, {
248
+ id: "ai_step",
249
+ name: "AI Step",
250
+ description: "",
251
+ platform: "unknown",
252
+ steps: [step],
253
+ version: "0",
254
+ tags: [],
255
+ successCount: 0,
256
+ failCount: 0,
257
+ });
258
+ if (stepResult.success) {
259
+ executedSteps.push(step);
260
+ this.log(`AI step ${executedSteps.length}: ${step.description ?? step.action}`);
261
+ }
262
+ else {
263
+ this.log(`AI step failed: ${stepResult.error}`);
264
+ }
265
+ await sleep(300);
266
+ }
267
+ catch (err) {
268
+ this.log(`AI step error: ${err instanceof Error ? err.message : String(err)}`);
269
+ }
270
+ }
271
+ return {
272
+ playbook: "ai_generated",
273
+ success: false,
274
+ stepsCompleted: executedSteps.length,
275
+ totalSteps: -1,
276
+ failedAtStep: executedSteps.length,
277
+ error: "Max AI steps reached",
278
+ durationMs: Date.now() - start,
279
+ };
280
+ }
281
+ /**
282
+ * Patch a playbook — insert recovery steps at the failure point.
283
+ */
284
+ patchPlaybook(playbook, failedAt, recoverySteps) {
285
+ const patched = {
286
+ ...playbook,
287
+ steps: [
288
+ ...playbook.steps.slice(0, failedAt),
289
+ ...recoverySteps,
290
+ ...playbook.steps.slice(failedAt),
291
+ ],
292
+ version: bumpVersion(playbook.version),
293
+ };
294
+ this.store.save(patched);
295
+ this.log(`Patched playbook ${playbook.id}: inserted ${recoverySteps.length} recovery steps at position ${failedAt}`);
296
+ }
297
+ /**
298
+ * Save AI-generated steps as a new playbook.
299
+ */
300
+ saveNewPlaybook(task, steps) {
301
+ const id = `auto_${Date.now()}`;
302
+ const playbook = {
303
+ id,
304
+ name: task.slice(0, 80),
305
+ description: `Auto-generated from AI execution: ${task}`,
306
+ platform: "unknown",
307
+ steps,
308
+ version: "1.0.0",
309
+ tags: task.toLowerCase().split(/\W+/).filter((w) => w.length >= 3),
310
+ successCount: 1,
311
+ failCount: 0,
312
+ lastRun: new Date().toISOString(),
313
+ };
314
+ this.store.save(playbook);
315
+ this.log(`Saved new playbook: ${id} (${steps.length} steps)`);
316
+ }
317
+ /** Get all loaded playbooks. */
318
+ listPlaybooks() {
319
+ return this.store.getAll();
320
+ }
321
+ /** Reload playbooks from disk. */
322
+ reload() {
323
+ this.store.load();
324
+ }
325
+ }
326
+ function sleep(ms) {
327
+ return new Promise((resolve) => setTimeout(resolve, ms));
328
+ }
329
+ function bumpVersion(version) {
330
+ const parts = version.split(".").map(Number);
331
+ if (parts.length === 3) {
332
+ parts[2]++;
333
+ return parts.join(".");
334
+ }
335
+ return version + ".1";
336
+ }
337
+ /**
338
+ * Build a context string from playbook metadata for AI consumption.
339
+ * Includes selectors, flows, known errors, detection expressions.
340
+ */
341
+ function buildPlaybookContext(playbook) {
342
+ const parts = [];
343
+ if (playbook.urls && Object.keys(playbook.urls).length > 0) {
344
+ parts.push("URLS:\n" + Object.entries(playbook.urls).map(([k, v]) => ` ${k}: ${v}`).join("\n"));
345
+ }
346
+ if (playbook.selectors && Object.keys(playbook.selectors).length > 0) {
347
+ parts.push("SELECTORS:");
348
+ for (const [group, sels] of Object.entries(playbook.selectors)) {
349
+ parts.push(` [${group}]`);
350
+ for (const [name, sel] of Object.entries(sels)) {
351
+ parts.push(` ${name}: ${sel}`);
352
+ }
353
+ }
354
+ }
355
+ if (playbook.flows && Object.keys(playbook.flows).length > 0) {
356
+ parts.push("FLOWS:");
357
+ for (const [name, flow] of Object.entries(playbook.flows)) {
358
+ parts.push(` [${name}]`);
359
+ for (const step of flow.steps) {
360
+ parts.push(` - ${step}`);
361
+ }
362
+ if (flow.guards) {
363
+ parts.push(` Guards:`);
364
+ for (const g of flow.guards)
365
+ parts.push(` ! ${g}`);
366
+ }
367
+ }
368
+ }
369
+ if (playbook.detection && Object.keys(playbook.detection).length > 0) {
370
+ parts.push("DETECTION (JS expressions):");
371
+ for (const [name, expr] of Object.entries(playbook.detection)) {
372
+ parts.push(` ${name}: ${expr}`);
373
+ }
374
+ }
375
+ if (playbook.errors && playbook.errors.length > 0) {
376
+ parts.push("KNOWN ERRORS & SOLUTIONS:");
377
+ for (const e of playbook.errors) {
378
+ parts.push(` [${e.severity}] ${e.error}`);
379
+ parts.push(` Context: ${e.context}`);
380
+ parts.push(` Solution: ${e.solution}`);
381
+ }
382
+ }
383
+ if (playbook.policyNotes && Object.keys(playbook.policyNotes).length > 0) {
384
+ parts.push("POLICY NOTES:");
385
+ for (const [cat, notes] of Object.entries(playbook.policyNotes)) {
386
+ parts.push(` [${cat}]`);
387
+ for (const n of notes)
388
+ parts.push(` - ${n}`);
389
+ }
390
+ }
391
+ return parts.join("\n");
392
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Playbook Store — load, save, match playbooks from disk
3
+ *
4
+ * Playbooks are stored as JSON files in the playbooks/ directory.
5
+ * Each file can contain a Playbook directly, or a legacy format
6
+ * that gets converted.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ export class PlaybookStore {
11
+ dir;
12
+ playbooks = new Map();
13
+ constructor(dir) {
14
+ this.dir = dir;
15
+ }
16
+ /** Load all playbooks from disk into memory. */
17
+ load() {
18
+ this.playbooks.clear();
19
+ if (!fs.existsSync(this.dir))
20
+ return;
21
+ const files = fs.readdirSync(this.dir).filter((f) => f.endsWith(".json"));
22
+ for (const file of files) {
23
+ try {
24
+ const raw = JSON.parse(fs.readFileSync(path.join(this.dir, file), "utf-8"));
25
+ const playbook = this.normalize(raw, file);
26
+ if (playbook) {
27
+ this.playbooks.set(playbook.id, playbook);
28
+ }
29
+ }
30
+ catch {
31
+ // Skip unparseable files
32
+ }
33
+ }
34
+ }
35
+ /** Get all loaded playbooks. */
36
+ getAll() {
37
+ return [...this.playbooks.values()];
38
+ }
39
+ /** Get a playbook by ID. */
40
+ get(id) {
41
+ return this.playbooks.get(id);
42
+ }
43
+ /** Find playbooks matching a URL. */
44
+ matchByUrl(url) {
45
+ return this.getAll().filter((p) => {
46
+ if (!p.urlPatterns || p.urlPatterns.length === 0)
47
+ return false;
48
+ return p.urlPatterns.some((pattern) => {
49
+ try {
50
+ return new RegExp(pattern).test(url);
51
+ }
52
+ catch {
53
+ return url.includes(pattern);
54
+ }
55
+ });
56
+ });
57
+ }
58
+ /** Find playbooks matching tags. */
59
+ matchByTags(tags) {
60
+ const tagSet = new Set(tags.map((t) => t.toLowerCase()));
61
+ return this.getAll()
62
+ .filter((p) => p.tags.some((t) => tagSet.has(t.toLowerCase())))
63
+ .sort((a, b) => b.successCount - a.successCount);
64
+ }
65
+ /** Find playbooks by platform. */
66
+ matchByPlatform(platform) {
67
+ return this.getAll()
68
+ .filter((p) => p.platform.toLowerCase() === platform.toLowerCase())
69
+ .sort((a, b) => b.successCount - a.successCount);
70
+ }
71
+ /** Find best playbook for a task description (simple keyword matching). */
72
+ matchByTask(task) {
73
+ const tokens = task.toLowerCase().split(/\W+/).filter((w) => w.length >= 3);
74
+ if (tokens.length === 0)
75
+ return null;
76
+ let best = null;
77
+ let bestScore = 0;
78
+ for (const p of this.playbooks.values()) {
79
+ const haystack = `${p.name} ${p.description} ${p.tags.join(" ")} ${p.platform}`.toLowerCase();
80
+ let score = 0;
81
+ for (const token of tokens) {
82
+ if (haystack.includes(token))
83
+ score++;
84
+ }
85
+ // Weight by reliability
86
+ const reliability = p.successCount + p.failCount > 0
87
+ ? p.successCount / (p.successCount + p.failCount)
88
+ : 0.5;
89
+ score *= reliability;
90
+ if (score > bestScore) {
91
+ bestScore = score;
92
+ best = p;
93
+ }
94
+ }
95
+ return bestScore > 0 ? best : null;
96
+ }
97
+ /** Save a playbook to disk. */
98
+ save(playbook) {
99
+ if (!fs.existsSync(this.dir)) {
100
+ fs.mkdirSync(this.dir, { recursive: true });
101
+ }
102
+ const filename = `${playbook.id}.json`;
103
+ fs.writeFileSync(path.join(this.dir, filename), JSON.stringify(playbook, null, 2) + "\n");
104
+ this.playbooks.set(playbook.id, playbook);
105
+ }
106
+ /** Record a run outcome. */
107
+ recordOutcome(id, success) {
108
+ const playbook = this.playbooks.get(id);
109
+ if (!playbook)
110
+ return;
111
+ if (success) {
112
+ playbook.successCount++;
113
+ }
114
+ else {
115
+ playbook.failCount++;
116
+ }
117
+ playbook.lastRun = new Date().toISOString();
118
+ this.save(playbook);
119
+ }
120
+ /**
121
+ * Normalize raw JSON into a Playbook.
122
+ * Handles both new format (has steps array) and legacy format (has flows object).
123
+ */
124
+ normalize(raw, filename) {
125
+ // Already in new format
126
+ if (Array.isArray(raw.steps)) {
127
+ return raw;
128
+ }
129
+ // Legacy format: has flows with steps arrays (like instagram_v2.json)
130
+ if (raw.flows && typeof raw.flows === "object") {
131
+ return this.convertLegacy(raw, filename);
132
+ }
133
+ return null;
134
+ }
135
+ /**
136
+ * Convert legacy playbook format to new format.
137
+ * Preserves all rich metadata: selectors, flows, errors, detection, policy notes.
138
+ */
139
+ convertLegacy(raw, filename) {
140
+ const platform = raw.platform ?? filename.replace(".json", "");
141
+ const id = raw.playbook ?? filename.replace(".json", "");
142
+ const description = raw.description ?? "";
143
+ return {
144
+ id,
145
+ name: `${platform} playbook`,
146
+ description,
147
+ platform,
148
+ urlPatterns: raw.urls ? Object.values(raw.urls).map(escapeRegex) : [],
149
+ steps: [], // Legacy playbooks use flows instead of direct steps
150
+ tags: [platform, ...(raw.playbook ? [raw.playbook] : [])],
151
+ version: raw.version ?? "1.0.0",
152
+ successCount: 0,
153
+ failCount: 0,
154
+ // Preserve rich metadata
155
+ ...(raw.urls ? { urls: raw.urls } : {}),
156
+ ...(raw.selectors ? { selectors: raw.selectors } : {}),
157
+ ...(raw.flows ? { flows: raw.flows } : {}),
158
+ ...(raw.detection ? { detection: raw.detection } : {}),
159
+ ...(raw.errors ? { errors: raw.errors } : {}),
160
+ ...(raw.policy_notes ? { policyNotes: raw.policy_notes } : {}),
161
+ };
162
+ }
163
+ }
164
+ function escapeRegex(s) {
165
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Playbook types — executable automation recipes
3
+ */
4
+ export {};