@monoes/monobrowse 1.0.1 → 1.0.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.
@@ -5,23 +5,23 @@ import { join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { homedir } from 'node:os';
7
7
  import { createRequire } from 'node:module';
8
- import type { StepEvent, RunRecord, PlaybookDef } from '@monoes/monoplaybook';
9
8
 
10
- const RUNS_FILE = join(homedir(), '.monomind', 'browse-runs.json');
11
- const PLAYBOOKS_FILE = join(homedir(), '.monomind', 'playbooks.json');
12
-
13
- async function loadPlaybooks(): Promise<PlaybookDef[]> {
14
- if (!existsSync(PLAYBOOKS_FILE)) return [];
15
- try {
16
- return JSON.parse(await readFile(PLAYBOOKS_FILE, 'utf-8')) as PlaybookDef[];
17
- } catch { return []; }
9
+ export interface StepEvent {
10
+ type: string;
11
+ projectDir?: string;
12
+ [key: string]: unknown;
18
13
  }
19
14
 
20
- async function savePlaybooks(playbooks: PlaybookDef[]): Promise<void> {
21
- await mkdir(join(homedir(), '.monomind'), { recursive: true });
22
- await writeFile(PLAYBOOKS_FILE, JSON.stringify(playbooks, null, 2));
15
+ export interface RunRecord {
16
+ id: string;
17
+ startedAt: number;
18
+ status: string;
19
+ itemsProcessed?: number;
20
+ [key: string]: unknown;
23
21
  }
24
22
 
23
+ const RUNS_FILE = join(homedir(), '.monomind', 'browse-runs.json');
24
+
25
25
  function readBody(req: IncomingMessage): Promise<string> {
26
26
  return new Promise((resolve, reject) => {
27
27
  let body = '';
@@ -90,7 +90,6 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
90
90
  }
91
91
 
92
92
  if (url === '/runs' && req.method === 'GET') {
93
- // Merge in-memory runs with persisted runs so the UI shows history even across restarts
94
93
  const persisted = await loadPersistedRuns().catch(() => [] as RunRecord[]);
95
94
  const seen = new Set(runHistory.map(r => r.id));
96
95
  const merged = [...runHistory, ...persisted.filter(r => !seen.has(r.id))]
@@ -109,12 +108,9 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
109
108
  return;
110
109
  }
111
110
 
112
- // Parse the URL to correctly handle query parameters such as ?dir=
113
111
  const parsed = new URL(url, 'http://localhost');
114
112
  if (parsed.pathname === '/events' && (req.method === 'GET' || req.method === 'HEAD') && !WebSocketServer) {
115
- // SSE endpoint (fallback when ws not available).
116
- // No CORS header — the dashboard is served from 127.0.0.1:4242 and no cross-origin
117
- // access is needed. A wildcard ACAO would let any web page subscribe to playbook events.
113
+ // SSE fallback when ws not available
118
114
  const subscribedDir = parsed.searchParams.get('dir') ?? null;
119
115
  res.writeHead(200, {
120
116
  'Content-Type': 'text/event-stream',
@@ -123,7 +119,6 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
123
119
  });
124
120
  res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
125
121
  clientDirs.set(res, subscribedDir);
126
- // 30-second keep-alive heartbeat prevents proxy idle-timeout drops.
127
122
  const heartbeat = setInterval(() => {
128
123
  try { res.write(': keep-alive\n\n'); } catch { clearInterval(heartbeat); }
129
124
  }, 30_000);
@@ -134,176 +129,6 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
134
129
  return;
135
130
  }
136
131
 
137
- // ── Playbook CRUD ────────────────────────────────────────────────────
138
- // GET /api/playbooks → list all saved playbooks
139
- // POST /api/playbooks → create/update a playbook (body: PlaybookDef)
140
- // GET /api/playbooks/:id → get single playbook
141
- // DELETE /api/playbooks/:id → delete a playbook
142
- // POST /api/playbooks/:id/run → run a playbook (delegates to engine)
143
- if (parsed.pathname === '/api/playbooks' && req.method === 'GET') {
144
- const list = await loadPlaybooks().catch(() => [] as PlaybookDef[]);
145
- res.writeHead(200, { 'Content-Type': 'application/json' });
146
- res.end(JSON.stringify(list));
147
- return;
148
- }
149
-
150
- if (parsed.pathname === '/api/playbooks' && req.method === 'POST') {
151
- try {
152
- const raw = await readBody(req);
153
- const pb = JSON.parse(raw) as PlaybookDef;
154
- if (!pb.id || !pb.name) {
155
- res.writeHead(400, { 'Content-Type': 'application/json' });
156
- res.end(JSON.stringify({ error: 'id and name are required' }));
157
- return;
158
- }
159
- const list = await loadPlaybooks().catch(() => [] as PlaybookDef[]);
160
- const idx = list.findIndex(w => w.id === pb.id);
161
- if (idx >= 0) list[idx] = pb; else list.push(pb);
162
- await savePlaybooks(list);
163
- res.writeHead(200, { 'Content-Type': 'application/json' });
164
- res.end(JSON.stringify(pb));
165
- } catch (e) {
166
- res.writeHead(500, { 'Content-Type': 'application/json' });
167
- res.end(JSON.stringify({ error: String(e) }));
168
- }
169
- return;
170
- }
171
-
172
- const pbIdMatch = parsed.pathname.match(/^\/api\/playbooks\/([^/]+)$/);
173
- const pbRunMatch = parsed.pathname.match(/^\/api\/playbooks\/([^/]+)\/run$/);
174
-
175
- if (pbRunMatch && req.method === 'POST') {
176
- const pbId = pbRunMatch[1];
177
- const list = await loadPlaybooks().catch(() => [] as PlaybookDef[]);
178
- const pb = list.find(w => w.id === pbId);
179
- if (!pb) {
180
- res.writeHead(404, { 'Content-Type': 'application/json' });
181
- res.end(JSON.stringify({ error: `Playbook ${pbId} not found` }));
182
- return;
183
- }
184
- // Import engine and builtin handlers dynamically to avoid circular deps at startup
185
- res.writeHead(202, { 'Content-Type': 'application/json' });
186
- res.end(JSON.stringify({ ok: true, playbookId: pbId, message: 'Playbook run started' }));
187
- // Run async, broadcast events via dashboard
188
- Promise.resolve().then(async () => {
189
- try {
190
- const { runPlaybook, createDefaultHandlers } = await import('../../index.js');
191
- const handlers = createDefaultHandlers();
192
- const record = await runPlaybook(pb, {
193
- handlers,
194
- onEvent: (evt) => { instance?.broadcast(evt); },
195
- isStopRequested: (id) => instance?.isStopRequested(id) ?? false,
196
- });
197
- instance?.addRunRecord(record);
198
- } catch (err) {
199
- console.error('[dashboard] playbook run error:', err);
200
- }
201
- });
202
- return;
203
- }
204
-
205
- if (pbIdMatch && req.method === 'GET') {
206
- const pbId = pbIdMatch[1];
207
- const list = await loadPlaybooks().catch(() => [] as PlaybookDef[]);
208
- const pb = list.find(w => w.id === pbId);
209
- if (!pb) { res.writeHead(404); res.end('Not found'); return; }
210
- res.writeHead(200, { 'Content-Type': 'application/json' });
211
- res.end(JSON.stringify(pb));
212
- return;
213
- }
214
-
215
- if (pbIdMatch && req.method === 'DELETE') {
216
- const pbId = pbIdMatch[1];
217
- const list = await loadPlaybooks().catch(() => [] as PlaybookDef[]);
218
- const newList = list.filter(w => w.id !== pbId);
219
- await savePlaybooks(newList);
220
- res.writeHead(200, { 'Content-Type': 'application/json' });
221
- res.end(JSON.stringify({ ok: true, deleted: pbId }));
222
- return;
223
- }
224
-
225
- // ── Webhook trigger ──────────────────────────────────────────────────────
226
- // POST /webhooks/:playbookId
227
- // Body (optional): JSON object or array — merged as `params` into the playbook run.
228
- // Header (optional): X-Webhook-Signature: sha256=<hex> for HMAC verification.
229
- // Response: 202 { ok, runId, playbookId } — run executes asynchronously.
230
- const webhookMatch = parsed.pathname.match(/^\/webhooks\/([^/]+)$/);
231
- if (webhookMatch && req.method === 'POST') {
232
- const pbId = webhookMatch[1];
233
- const list = await loadPlaybooks().catch(() => [] as PlaybookDef[]);
234
- const pb = list.find(w => w.id === pbId);
235
- if (!pb) {
236
- res.writeHead(404, { 'Content-Type': 'application/json' });
237
- res.end(JSON.stringify({ error: `Playbook ${pbId} not found` }));
238
- return;
239
- }
240
-
241
- const rawBody = await readBody(req).catch(() => '');
242
-
243
- // Optional HMAC-SHA256 signature verification
244
- const sigHeader = (req.headers['x-webhook-signature'] as string | undefined) ?? '';
245
- if (sigHeader) {
246
- try {
247
- const { createHmac } = await import('node:crypto');
248
- // Secret is stored in the playbook's trigger config (first schedule/webhook trigger)
249
- const triggers = (pb as unknown as Record<string, unknown>)['triggers'];
250
- const secret: string | undefined =
251
- Array.isArray(triggers)
252
- ? (triggers as Array<Record<string, unknown>>).find(t => t['secret'])?.[
253
- 'secret'
254
- ] as string | undefined
255
- : undefined;
256
- if (secret) {
257
- const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
258
- if (sigHeader !== expected) {
259
- res.writeHead(403, { 'Content-Type': 'application/json' });
260
- res.end(JSON.stringify({ error: 'Invalid webhook signature' }));
261
- return;
262
- }
263
- }
264
- } catch {
265
- // crypto not available or other error — skip verification
266
- }
267
- }
268
-
269
- // Parse body into params (string values only, per engine contract)
270
- const params: Record<string, string> = {};
271
- try {
272
- if (rawBody.trim()) {
273
- const parsed2 = JSON.parse(rawBody);
274
- if (parsed2 && typeof parsed2 === 'object' && !Array.isArray(parsed2)) {
275
- for (const [k, v] of Object.entries(parsed2 as Record<string, unknown>)) {
276
- params[k] = typeof v === 'string' ? v : JSON.stringify(v);
277
- }
278
- }
279
- params['__body'] = rawBody;
280
- }
281
- } catch {
282
- params['__body'] = rawBody;
283
- }
284
-
285
- const runId = `webhook-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
286
- res.writeHead(202, { 'Content-Type': 'application/json' });
287
- res.end(JSON.stringify({ ok: true, runId, playbookId: pbId }));
288
-
289
- Promise.resolve().then(async () => {
290
- try {
291
- const { runPlaybook, createDefaultHandlers } = await import('../../index.js');
292
- const handlers = createDefaultHandlers();
293
- const record = await runPlaybook(pb, {
294
- handlers,
295
- params,
296
- onEvent: (evt) => { instance?.broadcast(evt); },
297
- isStopRequested: (id) => instance?.isStopRequested(id) ?? false,
298
- });
299
- instance?.addRunRecord(record);
300
- } catch (err) {
301
- console.error('[dashboard] webhook run error:', err);
302
- }
303
- });
304
- return;
305
- }
306
-
307
132
  res.writeHead(404);
308
133
  res.end('Not found');
309
134
  });
@@ -311,12 +136,10 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
311
136
  if (WebSocketServer) {
312
137
  const wss = new WebSocketServer({ server });
313
138
  wss.on('connection', async (ws: any, req: any) => {
314
- // Extract optional project-dir filter from the WebSocket upgrade URL
315
139
  const upgradeUrl = req?.url ?? '/ws';
316
140
  const upgradeParsed = new URL(upgradeUrl, 'http://localhost');
317
141
  const subscribedDir = upgradeParsed.searchParams.get('dir') ?? null;
318
142
  clientDirs.set(ws, subscribedDir);
319
- // Merge in-memory and persisted runs so history survives server restarts
320
143
  const persisted = await loadPersistedRuns().catch(() => [] as RunRecord[]);
321
144
  const seen = new Set(runHistory.map(r => r.id));
322
145
  const merged = [...runHistory, ...persisted.filter(r => !seen.has(r.id))]
@@ -329,11 +152,10 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
329
152
 
330
153
  server.on('error', (err: NodeJS.ErrnoException) => {
331
154
  if (err.code === 'EADDRINUSE') {
332
- console.log(`Dashboard port ${port} in use — playbook will run without live dashboard.`);
155
+ console.log(`Dashboard port ${port} in use — continuing without live dashboard.`);
333
156
  } else {
334
157
  console.error(`Dashboard server error: ${err.message}`);
335
158
  }
336
- // Replace with a no-op instance so the engine doesn't crash
337
159
  instance = {
338
160
  broadcast: () => {},
339
161
  addRunRecord: () => {},
@@ -348,17 +170,14 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
348
170
  function broadcast(event: StepEvent): void {
349
171
  const msg = JSON.stringify(event);
350
172
  for (const [client, subscribedDir] of clientDirs) {
351
- // Server-side project filtering: if the client subscribed with ?dir=<path>, only
352
- // deliver events whose projectDir matches. Clients without a dir filter (subscribedDir
353
- // === null) receive all events, preserving backward-compatibility with ui.html.
354
173
  if (subscribedDir !== null && event.projectDir !== undefined && subscribedDir !== event.projectDir) {
355
174
  continue;
356
175
  }
357
176
  try {
358
177
  if (typeof client.send === 'function') {
359
- client.send(msg); // WebSocket
178
+ client.send(msg);
360
179
  } else {
361
- client.write(`data: ${msg}\n\n`); // SSE
180
+ client.write(`data: ${msg}\n\n`);
362
181
  }
363
182
  } catch {
364
183
  clientDirs.delete(client);
@@ -374,7 +193,6 @@ export function startDashboard(port = DEFAULT_PORT): DashboardServer {
374
193
  runHistory.unshift(record);
375
194
  if (runHistory.length > MAX_RUN_HISTORY) runHistory.pop();
376
195
  }
377
- // Persist to disk so the CLI dashboard (/api/workflow-runs) can read run history
378
196
  const dir = join(homedir(), '.monomind');
379
197
  mkdir(dir, { recursive: true }).then(() =>
380
198
  writeFile(RUNS_FILE, JSON.stringify(runHistory, null, 2))
package/src/cli/action.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  // src/commands/browse-action.ts
2
2
  import { Command } from 'commander';
3
- import { readdir, writeFile, mkdir } from 'node:fs/promises';
3
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
4
4
  import { existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { analyzePageForAction, type AnalyzerPage, readAction } from '../index.js';
6
+ import { analyzePageForAction, type AnalyzerPage } from '../index.js';
7
7
  import type { ActionDef } from '../index.js';
8
8
 
9
+ async function readAction(filePath: string): Promise<ActionDef> {
10
+ return JSON.parse(await readFile(filePath, 'utf8')) as ActionDef;
11
+ }
12
+
9
13
  // Built-in actions (shipped with the CLI) — actual step definitions live in adapters/
10
14
  const BUILTIN_ACTIONS: { id: string; platform: string; name: string }[] = [
11
15
  { id: 'linkedin:comment_post', platform: 'linkedin', name: 'Comment on Post' },
@@ -5,7 +5,6 @@
5
5
 
6
6
  import type { Command, CommandContext, CommandResult } from './types.js';
7
7
  import { output } from './output.js';
8
- import { createPlaybookCommand } from './playbook.js';
9
8
  import { createActionCommand } from './action.js';
10
9
  import { createPlatformCommand } from './platform.js';
11
10
  import type { CdpClient, ElementRef, NetworkRoute, FindAction } from '../index.js';
@@ -2734,7 +2733,6 @@ function wrapCommanderCommand(factory: () => import('commander').Command): Comma
2734
2733
  };
2735
2734
  }
2736
2735
 
2737
- const playbookSubcommand: Command = wrapCommanderCommand(createPlaybookCommand);
2738
2736
  const actionSubcommand: Command = wrapCommanderCommand(createActionCommand);
2739
2737
  const platformSubcommand: Command = wrapCommanderCommand(createPlatformCommand);
2740
2738
 
@@ -2806,7 +2804,6 @@ const browseCommand: Command = {
2806
2804
  harCommand,
2807
2805
  resizeCommand,
2808
2806
  closeCommand,
2809
- playbookSubcommand,
2810
2807
  actionSubcommand,
2811
2808
  platformSubcommand,
2812
2809
  ],
package/src/index.ts CHANGED
@@ -1,25 +1,5 @@
1
- // Re-export the full monoplaybook engine so consumers only need @monoes/monobrowse
2
- export * from '@monoes/monoplaybook';
3
-
4
1
  export * from './browser/index.js';
5
2
  export * from './browser/action-builder/analyzer.js';
6
3
  export * from './browser/action-builder/types.js';
7
4
  export * from './browser/adapters/index.js';
8
5
  export { startDashboard, getDashboard } from './browser/dashboard/server.js';
9
- export { createBuiltinHandlers, createBrowserHandlers } from './browser/playbook/index.js';
10
- export { readAction } from './browser/playbook/store.js';
11
-
12
- // Batteries-included handler factory: service nodes + browser automation + builtins.
13
- // This is the recommended entry point when using monobrowse as the default executor.
14
- import { createNodeHandlers } from '@monoes/monoplaybook';
15
- import { createBrowserHandlers } from './browser/playbook/browser-handlers.js';
16
- import { createBuiltinHandlers } from './browser/playbook/builtin-handlers.js';
17
- import type { NodeHandler } from '@monoes/monoplaybook';
18
-
19
- export function createDefaultHandlers(): Map<string, NodeHandler> {
20
- return new Map<string, NodeHandler>([
21
- ...createNodeHandlers(),
22
- ...createBrowserHandlers(),
23
- ...createBuiltinHandlers(),
24
- ]);
25
- }