@monoes/monobrowse 1.0.0 → 1.0.1

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 (101) hide show
  1. package/dist/package.json +75 -0
  2. package/dist/src/browser/adapters/index.d.ts.map +1 -1
  3. package/dist/src/browser/adapters/index.js +4 -0
  4. package/dist/src/browser/adapters/index.js.map +1 -1
  5. package/dist/src/browser/browser.js +2 -2
  6. package/dist/src/browser/browser.js.map +1 -1
  7. package/dist/src/browser/cdp.d.ts +6 -0
  8. package/dist/src/browser/cdp.d.ts.map +1 -1
  9. package/dist/src/browser/cdp.js +56 -1
  10. package/dist/src/browser/cdp.js.map +1 -1
  11. package/dist/src/browser/dashboard/api-types.d.ts +1 -1
  12. package/dist/src/browser/dashboard/server.d.ts +1 -1
  13. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  14. package/dist/src/browser/dashboard/server.js +204 -15
  15. package/dist/src/browser/dashboard/server.js.map +1 -1
  16. package/dist/src/browser/dashboard/ui.html +1811 -0
  17. package/dist/src/browser/playbook/browser-handlers.d.ts +3 -0
  18. package/dist/src/browser/playbook/browser-handlers.d.ts.map +1 -0
  19. package/dist/src/browser/playbook/browser-handlers.js +377 -0
  20. package/dist/src/browser/playbook/browser-handlers.js.map +1 -0
  21. package/dist/src/browser/playbook/builtin-handlers.d.ts +3 -0
  22. package/dist/src/browser/playbook/builtin-handlers.d.ts.map +1 -0
  23. package/dist/src/browser/playbook/builtin-handlers.js +364 -0
  24. package/dist/src/browser/playbook/builtin-handlers.js.map +1 -0
  25. package/dist/src/browser/playbook/index.d.ts +3 -0
  26. package/dist/src/browser/playbook/index.d.ts.map +1 -0
  27. package/dist/src/browser/playbook/index.js +3 -0
  28. package/dist/src/browser/playbook/index.js.map +1 -0
  29. package/dist/src/browser/playbook/store.d.ts +8 -0
  30. package/dist/src/browser/playbook/store.d.ts.map +1 -0
  31. package/dist/src/browser/playbook/store.js +87 -0
  32. package/dist/src/browser/playbook/store.js.map +1 -0
  33. package/dist/src/browser/screenshot.d.ts +2 -0
  34. package/dist/src/browser/screenshot.d.ts.map +1 -1
  35. package/dist/src/browser/screenshot.js +69 -2
  36. package/dist/src/browser/screenshot.js.map +1 -1
  37. package/dist/src/browser/types.d.ts.map +1 -1
  38. package/dist/src/browser/types.js +11 -0
  39. package/dist/src/browser/types.js.map +1 -1
  40. package/dist/src/cli/action.d.ts +3 -0
  41. package/dist/src/cli/action.d.ts.map +1 -0
  42. package/dist/src/cli/action.js +139 -0
  43. package/dist/src/cli/action.js.map +1 -0
  44. package/dist/src/cli/commands.d.ts +8 -0
  45. package/dist/src/cli/commands.d.ts.map +1 -0
  46. package/dist/src/cli/commands.js +3021 -0
  47. package/dist/src/cli/commands.js.map +1 -0
  48. package/dist/src/cli/output.d.ts +11 -0
  49. package/dist/src/cli/output.d.ts.map +1 -0
  50. package/dist/src/cli/output.js +33 -0
  51. package/dist/src/cli/output.js.map +1 -0
  52. package/dist/src/cli/platform.d.ts +7 -0
  53. package/dist/src/cli/platform.d.ts.map +1 -0
  54. package/dist/src/cli/platform.js +193 -0
  55. package/dist/src/cli/platform.js.map +1 -0
  56. package/dist/src/cli/playbook.d.ts +3 -0
  57. package/dist/src/cli/playbook.d.ts.map +1 -0
  58. package/dist/src/cli/playbook.js +265 -0
  59. package/dist/src/cli/playbook.js.map +1 -0
  60. package/dist/src/cli/types.d.ts +42 -0
  61. package/dist/src/cli/types.d.ts.map +1 -0
  62. package/dist/src/cli/types.js +2 -0
  63. package/dist/src/cli/types.js.map +1 -0
  64. package/dist/src/cli/workflow.d.ts +2 -0
  65. package/dist/src/cli/workflow.d.ts.map +1 -0
  66. package/dist/src/cli/workflow.js +3 -0
  67. package/dist/src/cli/workflow.js.map +1 -0
  68. package/dist/src/cli.d.ts +7 -0
  69. package/dist/src/cli.d.ts.map +1 -0
  70. package/dist/src/cli.js +187 -0
  71. package/dist/src/cli.js.map +1 -0
  72. package/dist/src/index.d.ts +5 -1
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +16 -1
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +24 -5
  78. package/src/browser/adapters/index.ts +4 -0
  79. package/src/browser/browser.ts +2 -2
  80. package/src/browser/cdp.ts +68 -1
  81. package/src/browser/dashboard/api-types.ts +1 -1
  82. package/src/browser/dashboard/server.ts +210 -17
  83. package/src/browser/dashboard/ui.html +1412 -45
  84. package/src/browser/playbook/browser-handlers.ts +449 -0
  85. package/src/browser/{workflow → playbook}/builtin-handlers.ts +35 -14
  86. package/src/browser/playbook/index.ts +2 -0
  87. package/src/browser/{workflow → playbook}/store.ts +14 -14
  88. package/src/browser/screenshot.ts +78 -3
  89. package/src/browser/types.ts +11 -0
  90. package/src/cli/action.ts +145 -0
  91. package/src/cli/commands.ts +2895 -0
  92. package/src/cli/output.ts +37 -0
  93. package/src/cli/platform.ts +211 -0
  94. package/src/cli/playbook.ts +280 -0
  95. package/src/cli/types.ts +47 -0
  96. package/src/cli.ts +208 -0
  97. package/src/index.ts +20 -1
  98. package/src/browser/workflow/engine.ts +0 -202
  99. package/src/browser/workflow/expression.ts +0 -107
  100. package/src/browser/workflow/index.ts +0 -5
  101. package/src/browser/workflow/types.ts +0 -62
@@ -0,0 +1,449 @@
1
+ // createBrowserHandlers — CDP-backed NodeHandler implementations for @monoes/monoplaybook.
2
+ //
3
+ // browser.open — connect to Chrome CDP and open a URL
4
+ // browser.navigate — navigate the current tab to a URL
5
+ // browser.click — click an element (selector | role | text | label | placeholder)
6
+ // browser.type — type text into an element (keystroke-by-keystroke)
7
+ // browser.fill — set an input value directly (faster than type)
8
+ // browser.select — pick an <option> by value or visible text
9
+ // browser.check — check or uncheck a checkbox / radio
10
+ // browser.hover — hover over an element
11
+ // browser.press — press a key or combo (e.g. "Enter", "Ctrl+A")
12
+ // browser.scroll — scroll up/down/left/right by pixel amount
13
+ // browser.wait — wait for selector | url | load | networkidle | duration
14
+ // browser.extract — pull text, attribute, html, all_text, url, or title into item.data
15
+ // browser.snapshot — capture an accessibility tree snapshot
16
+ // browser.screenshot — take a screenshot (saves to file or returns base64)
17
+ // browser.evaluate — run arbitrary JS; result goes into item.data
18
+ // browser.close — drop the cached CDP connection (does not kill Chrome)
19
+
20
+ import type { NodeHandler, Item } from '@monoes/monoplaybook';
21
+ import { CdpClient } from '../cdp.js';
22
+ import {
23
+ connectToTarget,
24
+ openUrl,
25
+ waitForLoad,
26
+ getCurrentUrl,
27
+ getCurrentTitle,
28
+ } from '../browser.js';
29
+ import {
30
+ clickElement,
31
+ fillElement,
32
+ typeText,
33
+ pressKey,
34
+ pressKeyCombo,
35
+ scrollElement,
36
+ hoverElement,
37
+ selectOption,
38
+ checkElement,
39
+ evaluateJs,
40
+ } from '../actions.js';
41
+ import {
42
+ findBySelector,
43
+ findByRole,
44
+ findByText,
45
+ findByLabel,
46
+ findByPlaceholder,
47
+ } from '../find.js';
48
+ import { waitFor } from '../wait.js';
49
+ import { captureSnapshot } from '../snapshot.js';
50
+ import { captureScreenshot } from '../screenshot.js';
51
+ import type { ElementRef } from '../types.js';
52
+ import { mkdir } from 'node:fs/promises';
53
+ import { dirname, resolve } from 'node:path';
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Shared connection state — cached per CDP port across nodes in one playbook run
57
+ // ---------------------------------------------------------------------------
58
+
59
+ interface BrowserConnection {
60
+ client: CdpClient;
61
+ sessionId: string;
62
+ refs: Map<string, ElementRef>;
63
+ }
64
+
65
+ const connectionCache = new Map<number, BrowserConnection>();
66
+
67
+ async function getConnection(port: number): Promise<BrowserConnection> {
68
+ if (connectionCache.has(port)) {
69
+ const cached = connectionCache.get(port)!;
70
+ try {
71
+ // Lightweight liveness check — if the CDP connection has dropped, this throws
72
+ await cached.client.send('Runtime.evaluate', { expression: '1', returnByValue: true }, cached.sessionId);
73
+ return cached;
74
+ } catch {
75
+ // Stale connection — evict and reconnect below
76
+ connectionCache.delete(port);
77
+ }
78
+ }
79
+ const { client, sessionId } = await connectToTarget(port);
80
+ const conn: BrowserConnection = { client, sessionId, refs: new Map() };
81
+ connectionCache.set(port, conn);
82
+ return conn;
83
+ }
84
+
85
+ function releaseConnection(port: number): void {
86
+ connectionCache.delete(port);
87
+ }
88
+
89
+ function safeResolvePath(rawPath: string): string {
90
+ const cwd = process.cwd();
91
+ const abs = resolve(cwd, rawPath);
92
+ if (abs !== cwd && !abs.startsWith(cwd + '/')) {
93
+ throw new Error(`Path traversal blocked: "${rawPath}" resolves outside working directory`);
94
+ }
95
+ return abs;
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Element resolution — locator types: selector | role | text | label | placeholder
100
+ // ---------------------------------------------------------------------------
101
+
102
+ async function findElement(
103
+ conn: BrowserConnection,
104
+ config: Record<string, unknown>,
105
+ ): Promise<ElementRef> {
106
+ const locator = (config['locator'] as string | undefined) ?? 'selector';
107
+ const value = config['value'] as string;
108
+ if (!value) throw new Error(`browser handler requires config.value (the element locator)`);
109
+
110
+ const { client, sessionId, refs } = conn;
111
+ const opts = {
112
+ exact: config['exact'] as boolean | undefined,
113
+ nth: config['nth'] as number | undefined,
114
+ };
115
+
116
+ let ref: ElementRef | null = null;
117
+ switch (locator) {
118
+ case 'selector': ref = await findBySelector(client, sessionId, refs, value); break;
119
+ case 'role': ref = await findByRole(client, sessionId, refs, value, opts); break;
120
+ case 'text': ref = await findByText(client, sessionId, refs, value, opts); break;
121
+ case 'label': ref = await findByLabel(client, sessionId, refs, value, opts); break;
122
+ case 'placeholder': ref = await findByPlaceholder(client, sessionId, refs, value, opts); break;
123
+ default: throw new Error(`Unknown locator "${locator}". Use: selector|role|text|label|placeholder`);
124
+ }
125
+
126
+ if (!ref) throw new Error(`Element not found: ${locator}="${value}"`);
127
+ return ref;
128
+ }
129
+
130
+ // Modifier bitmask used by CDP (matches DevTools protocol)
131
+ function modifierBits(modifiers: string[]): number {
132
+ let bits = 0;
133
+ for (const m of modifiers) {
134
+ switch (m.toLowerCase()) {
135
+ case 'alt': bits |= 1; break;
136
+ case 'ctrl':
137
+ case 'control': bits |= 2; break;
138
+ case 'meta':
139
+ case 'cmd': bits |= 4; break;
140
+ case 'shift': bits |= 8; break;
141
+ }
142
+ }
143
+ return bits;
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Handlers
148
+ // ---------------------------------------------------------------------------
149
+
150
+ export function createBrowserHandlers(): Map<string, NodeHandler> {
151
+ const h = new Map<string, NodeHandler>();
152
+
153
+ // ── browser.open ──────────────────────────────────────────────────────────
154
+ h.set('browser.open', async (items, config): Promise<Item[]> => {
155
+ const port = (config['port'] as number | undefined) ?? 9222;
156
+ const url = config['url'] as string;
157
+ if (!url) throw new Error('browser.open requires config.url');
158
+ const timeout = (config['timeoutMs'] as number | undefined) ?? 30000;
159
+ const condition = (config['waitUntil'] as 'load' | 'networkidle' | 'domcontentloaded' | undefined) ?? 'load';
160
+
161
+ const conn = await getConnection(port);
162
+ await openUrl(conn.client, conn.sessionId, url);
163
+ await waitForLoad(conn.client, conn.sessionId, condition, timeout);
164
+
165
+ const currentUrl = await getCurrentUrl(conn.client, conn.sessionId);
166
+ const title = await getCurrentTitle(conn.client, conn.sessionId);
167
+ return items.map(item => ({ ...item, data: { ...item.data, url: currentUrl, title, port } }));
168
+ });
169
+
170
+ // ── browser.navigate ──────────────────────────────────────────────────────
171
+ h.set('browser.navigate', async (items, config): Promise<Item[]> => {
172
+ const port = (config['port'] as number | undefined) ?? 9222;
173
+ const url = config['url'] as string;
174
+ if (!url) throw new Error('browser.navigate requires config.url');
175
+ const timeout = (config['timeoutMs'] as number | undefined) ?? 30000;
176
+ const condition = (config['waitUntil'] as 'load' | 'networkidle' | 'domcontentloaded' | undefined) ?? 'load';
177
+
178
+ const conn = await getConnection(port);
179
+ await openUrl(conn.client, conn.sessionId, url);
180
+ await waitForLoad(conn.client, conn.sessionId, condition, timeout);
181
+
182
+ const currentUrl = await getCurrentUrl(conn.client, conn.sessionId);
183
+ const title = await getCurrentTitle(conn.client, conn.sessionId);
184
+ return items.map(item => ({ ...item, data: { ...item.data, url: currentUrl, title } }));
185
+ });
186
+
187
+ // ── browser.click ─────────────────────────────────────────────────────────
188
+ h.set('browser.click', async (items, config): Promise<Item[]> => {
189
+ const port = (config['port'] as number | undefined) ?? 9222;
190
+ const conn = await getConnection(port);
191
+ const ref = await findElement(conn, config);
192
+ await clickElement(conn.client, conn.sessionId, ref);
193
+ return items;
194
+ });
195
+
196
+ // ── browser.type ──────────────────────────────────────────────────────────
197
+ h.set('browser.type', async (items, config): Promise<Item[]> => {
198
+ const port = (config['port'] as number | undefined) ?? 9222;
199
+ const text = config['text'] as string;
200
+ if (text === undefined) throw new Error('browser.type requires config.text');
201
+
202
+ const conn = await getConnection(port);
203
+ const ref = await findElement(conn, config);
204
+ if (config['clear'] !== false) {
205
+ await fillElement(conn.client, conn.sessionId, ref, '');
206
+ }
207
+ await typeText(conn.client, conn.sessionId, text);
208
+ return items;
209
+ });
210
+
211
+ // ── browser.fill ──────────────────────────────────────────────────────────
212
+ h.set('browser.fill', async (items, config): Promise<Item[]> => {
213
+ const port = (config['port'] as number | undefined) ?? 9222;
214
+ const text = (config['text'] as string | undefined) ?? '';
215
+
216
+ const conn = await getConnection(port);
217
+ const ref = await findElement(conn, config);
218
+ await fillElement(conn.client, conn.sessionId, ref, text);
219
+ return items;
220
+ });
221
+
222
+ // ── browser.select ────────────────────────────────────────────────────────
223
+ h.set('browser.select', async (items, config): Promise<Item[]> => {
224
+ const port = (config['port'] as number | undefined) ?? 9222;
225
+ const option = config['option'] as string;
226
+ if (!option) throw new Error('browser.select requires config.option');
227
+
228
+ const conn = await getConnection(port);
229
+ const ref = await findElement(conn, config);
230
+ await selectOption(conn.client, conn.sessionId, ref, option);
231
+ return items;
232
+ });
233
+
234
+ // ── browser.check ─────────────────────────────────────────────────────────
235
+ h.set('browser.check', async (items, config): Promise<Item[]> => {
236
+ const port = (config['port'] as number | undefined) ?? 9222;
237
+ const checked = (config['checked'] as boolean | undefined) ?? true;
238
+
239
+ const conn = await getConnection(port);
240
+ const ref = await findElement(conn, config);
241
+ await checkElement(conn.client, conn.sessionId, ref, checked);
242
+ return items;
243
+ });
244
+
245
+ // ── browser.hover ─────────────────────────────────────────────────────────
246
+ h.set('browser.hover', async (items, config): Promise<Item[]> => {
247
+ const port = (config['port'] as number | undefined) ?? 9222;
248
+ const conn = await getConnection(port);
249
+ const ref = await findElement(conn, config);
250
+ await hoverElement(conn.client, conn.sessionId, ref);
251
+ return items;
252
+ });
253
+
254
+ // ── browser.press ─────────────────────────────────────────────────────────
255
+ // Config: port, key — e.g. "Enter", "Tab", "Escape", "Ctrl+A", "Shift+Tab"
256
+ h.set('browser.press', async (items, config): Promise<Item[]> => {
257
+ const port = (config['port'] as number | undefined) ?? 9222;
258
+ const key = config['key'] as string;
259
+ if (!key) throw new Error('browser.press requires config.key');
260
+
261
+ const conn = await getConnection(port);
262
+ if (key.includes('+')) {
263
+ const parts = key.split('+').map(k => k.trim());
264
+ const mainKey = parts[parts.length - 1];
265
+ const bits = modifierBits(parts.slice(0, -1));
266
+ // Delegate to pressKeyCombo which uses resolveKey() for correct code/key mapping
267
+ // (avoids KeyENTER-style bugs from naive Key${mainKey.toUpperCase()} construction)
268
+ await pressKeyCombo(conn.client, conn.sessionId, mainKey, bits);
269
+ } else {
270
+ await pressKey(conn.client, conn.sessionId, key);
271
+ }
272
+ return items;
273
+ });
274
+
275
+ // ── browser.scroll ────────────────────────────────────────────────────────
276
+ // Config: port, direction (up|down|left|right, default "down"), amount (px, default 300),
277
+ // locator + value (optional — scroll a specific element)
278
+ h.set('browser.scroll', async (items, config): Promise<Item[]> => {
279
+ const port = (config['port'] as number | undefined) ?? 9222;
280
+ const direction = (config['direction'] as 'up' | 'down' | 'left' | 'right' | undefined) ?? 'down';
281
+ const amount = (config['amount'] as number | undefined) ?? 300;
282
+
283
+ const conn = await getConnection(port);
284
+ const hasLocator = !!(config['value']);
285
+
286
+ if (hasLocator) {
287
+ const ref = await findElement(conn, config);
288
+ await scrollElement(conn.client, conn.sessionId, direction, amount, ref);
289
+ } else {
290
+ await scrollElement(conn.client, conn.sessionId, direction, amount);
291
+ }
292
+ return items;
293
+ });
294
+
295
+ // ── browser.wait ──────────────────────────────────────────────────────────
296
+ // Config: port, for (selector|url|load|networkidle|duration), value, timeoutMs
297
+ h.set('browser.wait', async (items, config): Promise<Item[]> => {
298
+ const port = (config['port'] as number | undefined) ?? 9222;
299
+ const conn = await getConnection(port);
300
+ const waitKind = (config['for'] as string | undefined) ?? 'duration';
301
+ const timeout = (config['timeoutMs'] as number | undefined) ?? 30000;
302
+ const value = config['value'];
303
+
304
+ switch (waitKind) {
305
+ case 'duration':
306
+ await new Promise(res => setTimeout(res, (value as number | undefined) ?? 1000));
307
+ break;
308
+ case 'selector':
309
+ await waitFor(conn.client, conn.sessionId, { selector: value as string, timeout });
310
+ break;
311
+ case 'url':
312
+ await waitFor(conn.client, conn.sessionId, { url: value as string, timeout });
313
+ break;
314
+ case 'load':
315
+ await waitForLoad(conn.client, conn.sessionId, 'load', timeout);
316
+ break;
317
+ case 'networkidle':
318
+ await waitForLoad(conn.client, conn.sessionId, 'networkidle', timeout);
319
+ break;
320
+ default:
321
+ throw new Error(`browser.wait: unknown 'for' value: "${waitKind}". Use selector|url|load|networkidle|duration`);
322
+ }
323
+ return items;
324
+ });
325
+
326
+ // ── browser.extract ───────────────────────────────────────────────────────
327
+ // Config: port, as (text|attribute|html|all_text|url|title), locator, value,
328
+ // attribute (when as=attribute), outputKey (default "extracted")
329
+ h.set('browser.extract', async (items, config): Promise<Item[]> => {
330
+ const port = (config['port'] as number | undefined) ?? 9222;
331
+ const as = (config['as'] as string | undefined) ?? 'text';
332
+ const outputKey = (config['outputKey'] as string | undefined) ?? 'extracted';
333
+ const conn = await getConnection(port);
334
+
335
+ if (as === 'url') {
336
+ const url = await getCurrentUrl(conn.client, conn.sessionId);
337
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: url } }));
338
+ }
339
+ if (as === 'title') {
340
+ const title = await getCurrentTitle(conn.client, conn.sessionId);
341
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: title } }));
342
+ }
343
+ if (as === 'all_text') {
344
+ const text = await evaluateJs(conn.client, conn.sessionId, 'document.body.innerText');
345
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: text } }));
346
+ }
347
+
348
+ const ref = await findElement(conn, config);
349
+
350
+ if (as === 'text') {
351
+ const result = await conn.client.send<{ result: { value: unknown } }>(
352
+ 'Runtime.callFunctionOn',
353
+ { objectId: ref.objectId, functionDeclaration: 'function(){return this.innerText ?? this.textContent ?? ""}', returnByValue: true },
354
+ conn.sessionId,
355
+ );
356
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result.result.value } }));
357
+ }
358
+
359
+ if (as === 'attribute') {
360
+ const attr = config['attribute'] as string;
361
+ if (!attr) throw new Error('browser.extract with as=attribute requires config.attribute');
362
+ const result = await conn.client.send<{ result: { value: unknown } }>(
363
+ 'Runtime.callFunctionOn',
364
+ {
365
+ objectId: ref.objectId,
366
+ functionDeclaration: `function(){return this.getAttribute(${JSON.stringify(attr)})}`,
367
+ returnByValue: true,
368
+ },
369
+ conn.sessionId,
370
+ );
371
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result.result.value } }));
372
+ }
373
+
374
+ if (as === 'html') {
375
+ const result = await conn.client.send<{ result: { value: unknown } }>(
376
+ 'Runtime.callFunctionOn',
377
+ { objectId: ref.objectId, functionDeclaration: 'function(){return this.outerHTML}', returnByValue: true },
378
+ conn.sessionId,
379
+ );
380
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result.result.value } }));
381
+ }
382
+
383
+ throw new Error(`browser.extract: unknown 'as' value: "${as}". Use text|attribute|html|all_text|url|title`);
384
+ });
385
+
386
+ // ── browser.snapshot ──────────────────────────────────────────────────────
387
+ // Captures an accessibility tree. Output stored in item.data[outputKey].
388
+ // Config: port, selector (scope to element), interactiveOnly, compact, outputKey
389
+ h.set('browser.snapshot', async (items, config): Promise<Item[]> => {
390
+ const port = (config['port'] as number | undefined) ?? 9222;
391
+ const outputKey = (config['outputKey'] as string | undefined) ?? 'snapshot';
392
+ const conn = await getConnection(port);
393
+
394
+ const result = await captureSnapshot(conn.client, conn.sessionId, {
395
+ selector: config['selector'] as string | undefined,
396
+ interactiveOnly: (config['interactiveOnly'] as boolean | undefined) ?? false,
397
+ compact: (config['compact'] as boolean | undefined) ?? true,
398
+ });
399
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result } }));
400
+ });
401
+
402
+ // ── browser.screenshot ────────────────────────────────────────────────────
403
+ // Config: port, path (optional file path), fullPage, format (jpeg|png|webp),
404
+ // quality, outputKey (default "screenshot")
405
+ // Output: if path is set → saves file and puts path in data; else → puts dataUrl in data + binaryBase64
406
+ h.set('browser.screenshot', async (items, config): Promise<Item[]> => {
407
+ const port = (config['port'] as number | undefined) ?? 9222;
408
+ const outputKey = (config['outputKey'] as string | undefined) ?? 'screenshot';
409
+ const conn = await getConnection(port);
410
+
411
+ const userPath = config['path'] as string | undefined;
412
+ const absPath = userPath ? safeResolvePath(userPath) : undefined;
413
+ if (absPath) await mkdir(dirname(absPath), { recursive: true });
414
+
415
+ const { path: savedPath, dataUrl } = await captureScreenshot(conn.client, conn.sessionId, {
416
+ path: absPath,
417
+ fullPage: (config['fullPage'] as boolean | undefined) ?? false,
418
+ format: (config['format'] as 'jpeg' | 'png' | 'webp' | undefined) ?? 'png',
419
+ quality: config['quality'] as number | undefined,
420
+ });
421
+
422
+ return items.map(item => ({
423
+ ...item,
424
+ data: { ...item.data, [outputKey]: userPath ? savedPath : dataUrl },
425
+ }));
426
+ });
427
+
428
+ // ── browser.evaluate ──────────────────────────────────────────────────────
429
+ // Config: port, script (JS expression), outputKey (default "result")
430
+ h.set('browser.evaluate', async (items, config): Promise<Item[]> => {
431
+ const port = (config['port'] as number | undefined) ?? 9222;
432
+ const script = config['script'] as string;
433
+ if (!script) throw new Error('browser.evaluate requires config.script');
434
+ const outputKey = (config['outputKey'] as string | undefined) ?? 'result';
435
+
436
+ const conn = await getConnection(port);
437
+ const result = await evaluateJs(conn.client, conn.sessionId, script);
438
+ return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result } }));
439
+ });
440
+
441
+ // ── browser.close ─────────────────────────────────────────────────────────
442
+ h.set('browser.close', async (items, config): Promise<Item[]> => {
443
+ const port = (config['port'] as number | undefined) ?? 9222;
444
+ releaseConnection(port);
445
+ return items;
446
+ });
447
+
448
+ return h;
449
+ }
@@ -1,8 +1,8 @@
1
- // Built-in node handlers registered for every `browse workflow run` invocation.
1
+ // Built-in node handlers registered for every `browse playbook run` invocation.
2
2
  //
3
3
  // action.http — fetch a URL (GET/POST/etc.) and put the response in item.data
4
4
  // action.save_file — write item data or binaryBase64 to a file on disk
5
- // action.log — console.log each item (useful for debugging workflows)
5
+ // action.log — console.log each item (useful for debugging playbooks)
6
6
  // action.gemini_image — generate image via Gemini web app (browser automation + session store)
7
7
  // or Imagen REST API (GEMINI_API_KEY), or mock mode
8
8
  import { writeFile, readFile, mkdir, chmod } from 'node:fs/promises';
@@ -19,8 +19,7 @@ function safeResolvePath(rawPath: string): string {
19
19
  }
20
20
  return safePath;
21
21
  }
22
- import type { NodeHandler } from './engine.js';
23
- import type { Item } from './types.js';
22
+ import type { NodeHandler, Item } from '@monoes/monoplaybook';
24
23
 
25
24
  // ---------------------------------------------------------------------------
26
25
  // Session persistence (shared format with browse-platform.ts)
@@ -150,7 +149,7 @@ async function generateViaGeminiBrowser(
150
149
  console.log('│ A browser window has been opened to Gemini. │');
151
150
  console.log('│ Please sign in with your Google account. │');
152
151
  console.log('│ │');
153
- console.log('│ The workflow will resume automatically after login. │');
152
+ console.log('│ The playbook will resume automatically after login. │');
154
153
  console.log('│ Waiting up to 5 minutes… │');
155
154
  console.log('└─────────────────────────────────────────────────────────┘');
156
155
  console.log('');
@@ -238,24 +237,46 @@ export function createBuiltinHandlers(): Map<string, NodeHandler> {
238
237
  const handlers = new Map<string, NodeHandler>();
239
238
 
240
239
  // action.http
241
- // config: { url, method?, headers?, body?, responseField? }
242
- // Puts { statusCode, body, json? } into item.data[responseField ?? 'response']
240
+ // config: { url, method?, headers?, body?, responseField?, timeoutMs?, throwOnError? }
241
+ // Puts { statusCode, body, json? } into item.data[responseField ?? 'response'].
242
+ // throwOnError (default true): throws on 4xx/5xx responses.
243
+ // timeoutMs (default 30000): abort the request after this many ms.
243
244
  handlers.set('action.http', async (items, config) => {
244
245
  const url = String(config['url'] ?? '');
245
246
  const method = String(config['method'] ?? 'GET').toUpperCase();
246
247
  const headers = (config['headers'] as Record<string, string>) ?? {};
247
248
  const body = config['body'] !== undefined ? JSON.stringify(config['body']) : undefined;
248
249
  const responseField = String(config['responseField'] ?? 'response');
249
-
250
- const res = await fetch(url, {
251
- method,
252
- headers: { 'Content-Type': 'application/json', ...headers },
253
- body: method !== 'GET' && method !== 'HEAD' ? body : undefined,
254
- });
250
+ const timeoutMs = Number(config['timeoutMs'] ?? 30_000);
251
+ const throwOnError = config['throwOnError'] !== false; // default true
252
+
253
+ const controller = new AbortController();
254
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
255
+
256
+ let res: Response;
257
+ try {
258
+ res = await fetch(url, {
259
+ method,
260
+ headers: { 'Content-Type': 'application/json', ...headers },
261
+ body: method !== 'GET' && method !== 'HEAD' ? body : undefined,
262
+ signal: controller.signal,
263
+ });
264
+ } catch (err) {
265
+ clearTimeout(timer);
266
+ const msg = err instanceof Error && err.name === 'AbortError'
267
+ ? `action.http: request timed out after ${timeoutMs}ms — ${url}`
268
+ : `action.http: network error — ${err instanceof Error ? err.message : String(err)}`;
269
+ throw new Error(msg);
270
+ }
271
+ clearTimeout(timer);
255
272
 
256
273
  const text = await res.text();
257
274
  let json: unknown;
258
- try { json = JSON.parse(text); } catch { /* not JSON */ }
275
+ try { json = JSON.parse(text); } catch { /* not JSON — body is plain text or binary */ }
276
+
277
+ if (throwOnError && !res.ok) {
278
+ throw new Error(`action.http: HTTP ${res.status} ${res.statusText} — ${url}\n${text.slice(0, 500)}`);
279
+ }
259
280
 
260
281
  return items.map(item => ({
261
282
  ...item,
@@ -0,0 +1,2 @@
1
+ export * from './builtin-handlers.js';
2
+ export * from './browser-handlers.js';
@@ -1,11 +1,11 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import type { WorkflowDef, RunRecord } from './types.js';
2
+ import type { PlaybookDef, RunRecord } from '@monoes/monoplaybook';
3
3
  import type { ActionDef } from '../action-builder/types.js';
4
4
 
5
5
  // TODO: Upgrade to better-sqlite3 for persistence across process restarts.
6
6
  // Schema:
7
7
  // CREATE TABLE browse_runs (
8
- // id TEXT PRIMARY KEY, workflow_id TEXT, workflow_name TEXT,
8
+ // id TEXT PRIMARY KEY, playbook_id TEXT, playbook_name TEXT,
9
9
  // status TEXT, started_at INTEGER, completed_at INTEGER,
10
10
  // items_processed INTEGER, items_total INTEGER, error TEXT
11
11
  // );
@@ -19,25 +19,25 @@ export function clearRunStore(): void {
19
19
  runStore.clear();
20
20
  }
21
21
 
22
- export async function readWorkflow(filePath: string): Promise<WorkflowDef> {
22
+ export async function readPlaybook(filePath: string): Promise<PlaybookDef> {
23
23
  let raw: string;
24
24
  try {
25
25
  raw = await readFile(filePath, 'utf-8');
26
26
  } catch {
27
- throw new Error(`Workflow file not found: ${filePath}`);
27
+ throw new Error(`Playbook file not found: ${filePath}`);
28
28
  }
29
29
  let def: unknown;
30
30
  try {
31
31
  def = JSON.parse(raw);
32
32
  } catch {
33
- throw new Error(`Invalid JSON in workflow file: ${filePath}`);
33
+ throw new Error(`Invalid JSON in playbook file: ${filePath}`);
34
34
  }
35
- if (!isWorkflowDef(def)) {
35
+ if (!isPlaybookDef(def)) {
36
36
  const w = def as Record<string, unknown>;
37
- if (typeof w?.['id'] !== 'string') throw new Error('Workflow missing required field: id');
38
- if (typeof w?.['name'] !== 'string') throw new Error('Workflow missing required field: name');
39
- if (!Array.isArray(w?.['nodes'])) throw new Error('Workflow missing required field: nodes');
40
- throw new Error('Workflow missing required field: connections');
37
+ if (typeof w?.['id'] !== 'string') throw new Error('Playbook missing required field: id');
38
+ if (typeof w?.['name'] !== 'string') throw new Error('Playbook missing required field: name');
39
+ if (!Array.isArray(w?.['nodes'])) throw new Error('Playbook missing required field: nodes');
40
+ throw new Error('Playbook missing required field: connections');
41
41
  }
42
42
  return def;
43
43
  }
@@ -49,7 +49,7 @@ function validateAction(def: unknown): void {
49
49
  if (!Array.isArray(a['steps'])) throw new Error('Action missing required field: steps');
50
50
  }
51
51
 
52
- function isWorkflowDef(def: unknown): def is WorkflowDef {
52
+ function isPlaybookDef(def: unknown): def is PlaybookDef {
53
53
  if (typeof def !== 'object' || def === null) return false;
54
54
  const w = def as Record<string, unknown>;
55
55
  return (
@@ -77,11 +77,11 @@ export async function readAction(filePath: string): Promise<ActionDef> {
77
77
  return def as ActionDef;
78
78
  }
79
79
 
80
- export async function writeRunRecord(record: RunRecord): Promise<void> {
80
+ export async function writePlaybookRun(record: RunRecord): Promise<void> {
81
81
  runStore.set(record.id, { ...record });
82
82
  }
83
83
 
84
- export async function listRuns(workflowId?: string): Promise<RunRecord[]> {
84
+ export async function listPlaybookRuns(playbookId?: string): Promise<RunRecord[]> {
85
85
  const all = [...runStore.values()];
86
- return workflowId ? all.filter(r => r.workflowId === workflowId) : all;
86
+ return playbookId ? all.filter(r => r.playbookId === playbookId) : all;
87
87
  }