@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.
@@ -1,280 +0,0 @@
1
- // src/commands/browse-playbook.ts
2
- import { Command } from 'commander';
3
- import { readdir, writeFile, mkdir } from 'node:fs/promises';
4
- import { existsSync } from 'node:fs';
5
- import { join, resolve } from 'node:path';
6
- import { readPlaybook, listPlaybookRuns, writePlaybookRun, runPlaybook } from '@monoes/monoplaybook';
7
- import type { PlaybookDef } from '@monoes/monoplaybook';
8
- import { createDefaultHandlers } from '../index.js';
9
- import { startDashboard } from '../index.js';
10
-
11
- export function createPlaybookCommand(): Command {
12
- const cmd = new Command('playbook')
13
- .alias('workflow')
14
- .description('Manage browser playbooks (saved automation recipes)');
15
-
16
- cmd
17
- .command('create <name>')
18
- .description('Scaffold a new playbook JSON file')
19
- .option('--template <type>', 'Starter template: minimal | http | google-sheets | gmail | github | gemini-image', 'minimal')
20
- .action(async (name: string, opts: { template: string }) => {
21
- const dir = join(process.cwd(), '.monomind', 'playbooks');
22
- await mkdir(dir, { recursive: true });
23
- const file = join(dir, `${name}.json`);
24
- if (existsSync(file)) {
25
- console.error(`Playbook already exists: ${file}`);
26
- process.exit(1);
27
- }
28
-
29
- const humanName = name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
30
- let def: PlaybookDef;
31
-
32
- switch (opts.template) {
33
- case 'http':
34
- def = {
35
- id: name, name: humanName,
36
- description: 'Fetch data from an HTTP endpoint and save to file',
37
- nodes: [
38
- { id: 'trigger', type: 'trigger.manual', name: 'Start', config: { items: [{ data: { url: 'https://api.example.com/data' } }] } },
39
- { id: 'fetch', type: 'action.http', name: 'Fetch Data', config: { url: '{{$json.url}}', method: 'GET' }, onError: 'skip' },
40
- { id: 'log', type: 'action.log', name: 'Log Result', config: { label: 'result' } },
41
- { id: 'save', type: 'action.save_file', name: 'Save JSON', config: { path: `./output/${name}-result.json` } },
42
- ],
43
- connections: [{ from: 'trigger', to: 'fetch' }, { from: 'fetch', to: 'log' }, { from: 'log', to: 'save' }],
44
- };
45
- break;
46
-
47
- case 'google-sheets': {
48
- const accessRef = '{{params.access_token}}';
49
- def = {
50
- id: name, name: humanName,
51
- description: 'Read rows from Google Sheets and process them',
52
- params: {
53
- spreadsheet_id: { required: true, description: 'Google Sheets ID from URL' },
54
- range: { default: 'Sheet1', description: 'Cell range e.g. Sheet1!A:Z' },
55
- access_token: { required: true, description: 'OAuth2 access token for Google Sheets API' },
56
- },
57
- nodes: [
58
- { id: 'trigger', type: 'trigger.manual', name: 'Start', config: {} },
59
- { id: 'read', type: 'service.google_sheets', name: 'Read Sheet', config: { operation: 'read_rows', spreadsheet_id: '{{params.spreadsheet_id}}', range: '{{params.range}}', access_token: accessRef } },
60
- { id: 'log', type: 'action.log', name: 'Log Rows', config: { label: 'row' } },
61
- ],
62
- connections: [{ from: 'trigger', to: 'read' }, { from: 'read', to: 'log' }],
63
- };
64
- break;
65
- }
66
-
67
- case 'gmail': {
68
- const gmailAccessRef = '{{params.access_token}}';
69
- def = {
70
- id: name, name: humanName,
71
- description: 'Send emails via Gmail API',
72
- params: {
73
- to: { required: true, description: 'Recipient email address' },
74
- subject: { required: true, description: 'Email subject line' },
75
- body: { required: true, description: 'Email body text' },
76
- access_token: { required: true, description: 'OAuth2 access token for Gmail API' },
77
- },
78
- nodes: [
79
- { id: 'trigger', type: 'trigger.manual', name: 'Start', config: { items: [{ data: {} }] } },
80
- { id: 'send', type: 'service.gmail', name: 'Send Email', config: { operation: 'send_message', to: '{{params.to}}', subject: '{{params.subject}}', body: '{{params.body}}', access_token: gmailAccessRef } },
81
- { id: 'log', type: 'action.log', name: 'Log Result', config: { label: 'sent' } },
82
- ],
83
- connections: [{ from: 'trigger', to: 'send' }, { from: 'send', to: 'log' }],
84
- };
85
- break;
86
- }
87
-
88
- case 'github': {
89
- const ghTokenRef = '{{params.gh_token}}';
90
- def = {
91
- id: name, name: humanName,
92
- description: 'List GitHub issues and process them',
93
- params: {
94
- owner: { required: true, description: 'GitHub repo owner (user or org)' },
95
- repo: { required: true, description: 'GitHub repo name' },
96
- gh_token: { required: true, description: 'GitHub personal access token' },
97
- state: { default: 'open', description: 'Issue state: open | closed | all' },
98
- },
99
- nodes: [
100
- { id: 'trigger', type: 'trigger.manual', name: 'Start', config: { items: [{ data: {} }] } },
101
- { id: 'issues', type: 'service.github', name: 'List Issues', config: { operation: 'list_issues', owner: '{{params.owner}}', repo: '{{params.repo}}', token: ghTokenRef, state: '{{params.state}}' } },
102
- { id: 'log', type: 'action.log', name: 'Log Issues', config: { label: 'issue' } },
103
- ],
104
- connections: [{ from: 'trigger', to: 'issues' }, { from: 'issues', to: 'log' }],
105
- };
106
- break;
107
- }
108
-
109
- case 'gemini-image':
110
- def = {
111
- id: name, name: humanName,
112
- description: 'Generate images using Gemini via browser or API',
113
- params: { prompt: { required: true, description: 'Image generation prompt' } },
114
- nodes: [
115
- { id: 'trigger', type: 'trigger.manual', name: 'Start', config: { items: [{ data: {} }] } },
116
- { id: 'generate', type: 'action.gemini_image', name: 'Generate Image', config: { prompt: '{{params.prompt}}', outputPath: `./output/${name}.png` }, onError: 'skip' },
117
- { id: 'log', type: 'action.log', name: 'Log Result', config: { label: 'generated' } },
118
- ],
119
- connections: [{ from: 'trigger', to: 'generate' }, { from: 'generate', to: 'log' }],
120
- };
121
- break;
122
-
123
- default: // minimal
124
- def = {
125
- id: name, name: humanName,
126
- description: 'New playbook',
127
- nodes: [
128
- { id: 'trigger', type: 'trigger.manual', name: 'Start', config: { items: [{ data: { message: 'hello' } }] } },
129
- { id: 'log', type: 'action.log', name: 'Log', config: { label: name } },
130
- ],
131
- connections: [{ from: 'trigger', to: 'log' }],
132
- };
133
- }
134
-
135
- await writeFile(file, JSON.stringify(def, null, 2));
136
- console.log(`Created: ${file}`);
137
- console.log(`Template: ${opts.template}`);
138
- if (def.params) {
139
- console.log(`Params: ${Object.entries(def.params).map(([k, v]) => `${k}${v.required ? ' (required)' : ''}`).join(', ')}`);
140
- }
141
- console.log(`Run with: npx monomind browse playbook run ${file}`);
142
- });
143
-
144
- cmd
145
- .command('run <file>')
146
- .description('Execute a playbook and open the dashboard')
147
- .option('--port <number>', 'Dashboard port (default: MONOBROWSE_DASHBOARD_PORT or 4242)', '4242')
148
- .option('--no-keep', 'Exit immediately after run instead of keeping dashboard alive')
149
- .option('--params <pairs...>', 'Playbook params as key=value pairs e.g. --params name=Alice count=5')
150
- .option('--timeout <seconds>', 'Max run time in seconds (default: 900)', '900')
151
- .action(async (file: string, opts: { port: string; keep: boolean; params?: string[]; timeout: string }) => {
152
- const filePath = resolve(file);
153
- let def: PlaybookDef;
154
- try {
155
- def = await readPlaybook(filePath);
156
- } catch (err) {
157
- console.error(`Failed to load playbook: ${err instanceof Error ? err.message : String(err)}`);
158
- process.exit(1);
159
- }
160
-
161
- // Parse --params key=value pairs
162
- const params: Record<string, string> = {};
163
- for (const pair of opts.params ?? []) {
164
- const eq = pair.indexOf('=');
165
- if (eq > 0) params[pair.slice(0, eq)] = pair.slice(eq + 1);
166
- }
167
-
168
- // Validate required params
169
- if (def.params) {
170
- for (const [key, spec] of Object.entries(def.params)) {
171
- if (spec.required && !(key in params) && spec.default === undefined) {
172
- console.error(`Missing required playbook param: ${key}${spec.description ? ` (${spec.description})` : ''}`);
173
- process.exit(1);
174
- }
175
- // Apply defaults
176
- if (!(key in params) && spec.default !== undefined) {
177
- params[key] = String(spec.default);
178
- }
179
- }
180
- }
181
-
182
- const port = parseInt(process.env['MONOBROWSE_DASHBOARD_PORT'] ?? opts.port, 10);
183
- const dashboard = startDashboard(port);
184
- console.log(`Dashboard: http://localhost:${dashboard.port}`);
185
- console.log(`Running: ${def.name}`);
186
- if (Object.keys(params).length > 0) {
187
- console.log(`Params: ${JSON.stringify(params)}`);
188
- }
189
-
190
- const timeoutMs = parseInt(opts.timeout, 10) * 1000;
191
- // Resolve the project directory once so every event carries the same canonical tag.
192
- const projectDir = process.cwd();
193
- const record = await runPlaybook(def, {
194
- handlers: createDefaultHandlers(),
195
- onEvent: event => {
196
- dashboard.broadcast({ ...event, projectDir });
197
- if (event.eventType === 'step_completed' || event.eventType === 'step_failed') {
198
- const status = event.eventType === 'step_completed' ? '✓' : '✗';
199
- console.log(` ${status} ${event.nodeName}${event.durationMs ? ` (${event.durationMs}ms)` : ''}`);
200
- }
201
- },
202
- signal: AbortSignal.timeout(timeoutMs),
203
- params,
204
- isStopRequested: (id) => dashboard.isStopRequested(id),
205
- });
206
-
207
- dashboard.addRunRecord(record);
208
- await writePlaybookRun(record);
209
- console.log(`\nCompleted: ${record.status} — ${record.itemsProcessed} items`);
210
- if (record.error) console.error(`Error: ${record.error}`);
211
-
212
- if (opts.keep !== false) {
213
- console.log(`\nDashboard kept alive at http://localhost:${dashboard.port} — press Ctrl+C to exit`);
214
- process.on('SIGINT', () => { dashboard.close(); process.exit(0); });
215
- process.on('SIGTERM', () => { dashboard.close(); process.exit(0); });
216
- } else {
217
- process.exit(record.status === 'completed' ? 0 : 1);
218
- }
219
- });
220
-
221
- cmd
222
- .command('list')
223
- .description('List available playbooks and their last run status')
224
- .action(async () => {
225
- const dir = join(process.cwd(), '.monomind', 'playbooks');
226
- if (!existsSync(dir)) {
227
- console.log('No playbooks found. Create one with: browse playbook create <name>');
228
- return;
229
- }
230
- const files = (await readdir(dir)).filter(f => f.endsWith('.json'));
231
- if (files.length === 0) {
232
- console.log('No playbooks found.');
233
- return;
234
- }
235
- const runs = await listPlaybookRuns();
236
- for (const file of files) {
237
- const id = file.replace('.json', '');
238
- const lastRun = runs.find(r => r.playbookId === id);
239
- const status = lastRun ? `[${lastRun.status}]` : '[never run]';
240
- console.log(` ${id} ${status}`);
241
- }
242
- });
243
-
244
- cmd
245
- .command('status <run-id>')
246
- .description('Check the status of a specific run')
247
- .action(async (runId: string) => {
248
- const runs = await listPlaybookRuns();
249
- const run = runs.find(r => r.id === runId);
250
- if (!run) {
251
- console.error(`Run not found: ${runId}`);
252
- process.exit(1);
253
- }
254
- console.log(JSON.stringify(run, null, 2));
255
- });
256
-
257
- cmd
258
- .command('stop <run-id>')
259
- .description('Request cancellation of a running playbook via dashboard')
260
- .option('--port <number>', 'Dashboard port (default: MONOBROWSE_DASHBOARD_PORT or 4242)', '4242')
261
- .action(async (runId: string, opts: { port: string }) => {
262
- const port = parseInt(process.env['MONOBROWSE_DASHBOARD_PORT'] ?? opts.port, 10);
263
- // Try to signal via HTTP POST to the dashboard server
264
- try {
265
- const res = await fetch(`http://127.0.0.1:${port}/stop/${runId}`, { method: 'POST' });
266
- if (res.ok) {
267
- console.log(`Stop requested for run: ${runId}`);
268
- console.log(`Dashboard: http://localhost:${port}`);
269
- } else {
270
- console.error(`Dashboard returned ${res.status}. Is a playbook running on port ${port}?`);
271
- }
272
- } catch {
273
- console.error(`Could not reach dashboard on port ${port}. Make sure the playbook is running.`);
274
- console.error(`You can also use the Stop button in the dashboard: http://localhost:${port}`);
275
- process.exit(1);
276
- }
277
- });
278
-
279
- return cmd;
280
- }