@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,37 @@
1
+ const COLORS = {
2
+ reset: '\x1b[0m',
3
+ bold: '\x1b[1m',
4
+ green: '\x1b[32m',
5
+ yellow: '\x1b[33m',
6
+ blue: '\x1b[34m',
7
+ red: '\x1b[31m',
8
+ cyan: '\x1b[36m',
9
+ } as const;
10
+
11
+ class OutputFormatter {
12
+ private colorEnabled = process.stdout.isTTY ?? false;
13
+
14
+ private c(color: keyof typeof COLORS, text: string): string {
15
+ if (!this.colorEnabled) return text;
16
+ return `${COLORS[color]}${text}${COLORS.reset}`;
17
+ }
18
+
19
+ printSuccess(message: string): void {
20
+ console.log(this.c('green', '✓') + ' ' + message);
21
+ }
22
+
23
+ printError(message: string, details?: string): void {
24
+ console.error(this.c('red', '✗') + ' ' + message);
25
+ if (details) console.error(this.c('red', ' ' + details));
26
+ }
27
+
28
+ printWarning(message: string): void {
29
+ console.warn(this.c('yellow', '⚠') + ' ' + message);
30
+ }
31
+
32
+ printInfo(message: string): void {
33
+ console.log(this.c('cyan', 'ℹ') + ' ' + message);
34
+ }
35
+ }
36
+
37
+ export const output = new OutputFormatter();
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Browse Platform Command — Manage platform connections (LinkedIn, Instagram, X, Gemini)
3
+ * Sessions stored as JSON at ~/.monomind/sessions.json
4
+ */
5
+
6
+ import { Command } from 'commander';
7
+ import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises';
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { getAdapter } from '../index.js';
12
+
13
+ const CDP_PORT = Number(process.env['MONOMIND_CDP_PORT'] ?? 9222);
14
+
15
+ const SESSIONS_FILE = join(homedir(), '.monomind', 'sessions.json');
16
+
17
+ interface Session {
18
+ id: string; // "platform:username"
19
+ platform: string;
20
+ username: string;
21
+ cookies: string; // JSON-serialized
22
+ userAgent: string;
23
+ createdAt: number;
24
+ lastUsedAt: number;
25
+ }
26
+
27
+ async function loadSessions(): Promise<Session[]> {
28
+ if (!existsSync(SESSIONS_FILE)) return [];
29
+ try {
30
+ const raw = await readFile(SESSIONS_FILE, 'utf-8');
31
+ return JSON.parse(raw) as Session[];
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ async function saveSessions(sessions: Session[]): Promise<void> {
38
+ await mkdir(join(homedir(), '.monomind'), { recursive: true });
39
+ await writeFile(SESSIONS_FILE, JSON.stringify(sessions, null, 2));
40
+ // Restrict to owner-only (rw-------) since the file contains platform session cookies.
41
+ await chmod(SESSIONS_FILE, 0o600);
42
+ }
43
+
44
+ export function createPlatformCommand(): Command {
45
+ const cmd = new Command('platform').description('Manage platform connections');
46
+
47
+ cmd
48
+ .command('connect <platform>')
49
+ .description('Connect a platform account via browser — opens Chrome, waits for login, saves session')
50
+ .option('--port <number>', 'CDP port of running Chrome instance', String(CDP_PORT))
51
+ .option('--timeout <seconds>', 'Login timeout in seconds', '300')
52
+ .action(async (platform: string, opts: { port: string; timeout: string }) => {
53
+ const SUPPORTED = ['linkedin', 'instagram', 'x', 'gemini'];
54
+ if (!SUPPORTED.includes(platform)) {
55
+ console.error(`Unknown platform: ${platform}. Supported: ${SUPPORTED.join(', ')}`);
56
+ process.exit(1);
57
+ }
58
+
59
+ let adapter: ReturnType<typeof getAdapter>;
60
+ try { adapter = getAdapter(platform); } catch (err) {
61
+ console.error(err instanceof Error ? err.message : String(err));
62
+ process.exit(1);
63
+ }
64
+
65
+ const cdpPort = Number(opts.port);
66
+ const timeoutMs = Number(opts.timeout) * 1000;
67
+
68
+ // Lazy-load browser module
69
+ let browser: typeof import('@monoes/monobrowse');
70
+ try { browser = await import('@monoes/monobrowse'); } catch (err) {
71
+ console.error('Browser module unavailable:', err);
72
+ process.exit(1);
73
+ }
74
+
75
+ // Connect to Chrome (or launch if needed)
76
+ let conn: Awaited<ReturnType<typeof browser.connectToTarget>>;
77
+ try {
78
+ conn = await browser.connectToTarget(cdpPort);
79
+ console.log(`Connected to Chrome on port ${cdpPort}`);
80
+ } catch {
81
+ console.log(`Chrome not found on port ${cdpPort} — launching managed browser...`);
82
+ try {
83
+ const port = await browser.launchBrowser({ port: cdpPort, headless: false });
84
+ conn = await browser.connectToTarget(port);
85
+ } catch (err) {
86
+ console.error('Failed to connect or launch Chrome:', err);
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ const { client, sessionId: sid } = conn;
92
+
93
+ try {
94
+ const refs = new Map();
95
+
96
+ // Restore any existing cookies first so the user might already be logged in
97
+ const sessions = await loadSessions();
98
+ const existingSession = sessions.find(s => s.platform === platform);
99
+ if (existingSession?.cookies) {
100
+ try {
101
+ const cookies = JSON.parse(existingSession.cookies);
102
+ if (cookies.length > 0) await browser.setCookies(client, sid, cookies);
103
+ } catch { /* ignore */ }
104
+ }
105
+
106
+ // Navigate to login URL
107
+ console.log(`\nOpening ${platform} login page...`);
108
+ await browser.openUrl(client, sid, adapter.loginURL());
109
+
110
+ // Check if already logged in after cookie restore
111
+ const pageIface = {
112
+ evaluate: async <T>(expr: string) => browser.evaluateJs(client, sid, expr) as Promise<T>,
113
+ url: async () => browser.getCurrentUrl(client, sid).catch(() => ''),
114
+ };
115
+ let isLoggedIn = await adapter.isLoggedIn(pageIface).catch(() => false);
116
+
117
+ if (!isLoggedIn) {
118
+ console.log('');
119
+ console.log('┌──────────────────────────────────────────────────────┐');
120
+ console.log(`│ Please sign in to ${platform.padEnd(34)}│`);
121
+ console.log('│ │');
122
+ console.log('│ Complete the login in the browser window. │');
123
+ console.log(`│ Waiting up to ${Math.round(timeoutMs / 60000)} minutes for login… │`);
124
+ console.log('└──────────────────────────────────────────────────────┘');
125
+ console.log('');
126
+
127
+ const deadline = Date.now() + timeoutMs;
128
+ while (Date.now() < deadline) {
129
+ await new Promise(r => setTimeout(r, 2000));
130
+ isLoggedIn = await adapter.isLoggedIn(pageIface).catch(() => false);
131
+ if (isLoggedIn) break;
132
+ // Navigate to app URL if user manually browsed away from login
133
+ const currentUrl = await browser.getCurrentUrl(client, sid).catch(() => '');
134
+ if (!currentUrl.includes(new URL(adapter.loginURL()).hostname) &&
135
+ !currentUrl.includes(new URL(adapter.baseURL).hostname)) {
136
+ await browser.openUrl(client, sid, adapter.loginURL());
137
+ }
138
+ }
139
+
140
+ if (!isLoggedIn) {
141
+ console.error(`\nLogin timeout after ${Math.round(timeoutMs / 1000)}s. Session not saved.`);
142
+ process.exit(1);
143
+ }
144
+ }
145
+
146
+ console.log('Login detected! Capturing session...');
147
+
148
+ // Capture cookies and user agent
149
+ const liveCookies = await browser.getCookies(client, sid).catch(() => []);
150
+ const userAgent = await browser.evaluateJs(client, sid, 'navigator.userAgent').catch(() => 'unknown') as string;
151
+ const username = await adapter.extractUsername(pageIface).catch(() => `${platform}-user`);
152
+
153
+ // Save to sessions file
154
+ const sessionEntry: Session = {
155
+ id: `${platform}:${username}`,
156
+ platform,
157
+ username: username || `${platform}-user`,
158
+ cookies: JSON.stringify(liveCookies),
159
+ userAgent: String(userAgent),
160
+ createdAt: existingSession?.createdAt ?? Date.now(),
161
+ lastUsedAt: Date.now(),
162
+ };
163
+ const idx = sessions.findIndex(s => s.platform === platform);
164
+ if (idx >= 0) sessions[idx] = sessionEntry; else sessions.push(sessionEntry);
165
+ await saveSessions(sessions);
166
+
167
+ console.log(`\n✓ Session saved: ${sessionEntry.id}`);
168
+ console.log(` Cookies: ${liveCookies.length} captured`);
169
+ console.log(` File: ${SESSIONS_FILE}`);
170
+ console.log('');
171
+ console.log(`Session will be automatically used by: monomind browse playbook run`);
172
+ } finally {
173
+ client.close();
174
+ }
175
+ });
176
+
177
+ cmd
178
+ .command('list')
179
+ .description('List connected platform accounts')
180
+ .action(async () => {
181
+ const sessions = await loadSessions();
182
+ if (sessions.length === 0) {
183
+ console.log('No connected accounts. Run: browse platform connect <platform>');
184
+ return;
185
+ }
186
+ console.log(`\n${'ID'.padEnd(40)} ${'PLATFORM'.padEnd(12)} ${'USERNAME'.padEnd(30)} LAST USED`);
187
+ console.log('─'.repeat(95));
188
+ for (const s of sessions) {
189
+ const lastUsed = new Date(s.lastUsedAt).toLocaleString();
190
+ console.log(`${s.id.padEnd(40)} ${s.platform.padEnd(12)} ${s.username.padEnd(30)} ${lastUsed}`);
191
+ }
192
+ });
193
+
194
+ cmd
195
+ .command('disconnect <id>')
196
+ .description('Remove a saved platform session')
197
+ .action(async (id: string) => {
198
+ const sessions = await loadSessions();
199
+ const idx = sessions.findIndex(s => s.id === id);
200
+ if (idx < 0) {
201
+ console.error(`Session not found: ${id}`);
202
+ console.log('Run "browse platform list" to see available sessions.');
203
+ process.exit(1);
204
+ }
205
+ sessions.splice(idx, 1);
206
+ await saveSessions(sessions);
207
+ console.log(`Disconnected: ${id}`);
208
+ });
209
+
210
+ return cmd;
211
+ }
@@ -0,0 +1,280 @@
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
+ }
@@ -0,0 +1,47 @@
1
+ export interface CommandContext {
2
+ args: string[];
3
+ flags: ParsedFlags;
4
+ cwd: string;
5
+ interactive: boolean;
6
+ }
7
+
8
+ export interface ParsedFlags {
9
+ [key: string]: string | boolean | number | string[];
10
+ _: string[];
11
+ }
12
+
13
+ export interface Command {
14
+ name: string;
15
+ description: string;
16
+ aliases?: string[];
17
+ subcommands?: Command[];
18
+ options?: CommandOption[];
19
+ examples?: CommandExample[];
20
+ action?: CommandAction;
21
+ hidden?: boolean;
22
+ }
23
+
24
+ export interface CommandOption {
25
+ name: string;
26
+ short?: string;
27
+ description: string;
28
+ type: 'string' | 'boolean' | 'number' | 'array';
29
+ default?: unknown;
30
+ required?: boolean;
31
+ choices?: string[];
32
+ validate?: (value: unknown) => boolean | string;
33
+ }
34
+
35
+ export interface CommandExample {
36
+ command: string;
37
+ description: string;
38
+ }
39
+
40
+ export type CommandAction = (ctx: CommandContext) => Promise<CommandResult | void>;
41
+
42
+ export interface CommandResult {
43
+ success: boolean;
44
+ message?: string;
45
+ data?: unknown;
46
+ exitCode?: number;
47
+ }