@marktoflow/cli 2.0.0-alpha.10

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.
package/dist/index.js ADDED
@@ -0,0 +1,809 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * marktoflow CLI
4
+ *
5
+ * Universal automation framework with native MCP support.
6
+ */
7
+ import { Command } from 'commander';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { parseFile, WorkflowEngine, SDKRegistry, createSDKStepExecutor, StepStatus, WorkflowStatus, loadEnv, ToolRegistry, WorkflowBundle, Scheduler, TemplateRegistry, loadConfig, } from '@marktoflow/core';
13
+ import { registerIntegrations } from '@marktoflow/integrations';
14
+ import { workerCommand } from './worker.js';
15
+ import { triggerCommand } from './trigger.js';
16
+ import { runWorkflowWizard, listTemplates } from './commands/new.js';
17
+ import { runUpdateWizard, listAgents } from './commands/update.js';
18
+ import { parse as parseYaml } from 'yaml';
19
+ import { executeDryRun, displayDryRunSummary } from './commands/dry-run.js';
20
+ import { WorkflowDebugger, parseBreakpoints } from './commands/debug.js';
21
+ const VERSION = '2.0.0-alpha.7';
22
+ // Load environment variables from .env files on CLI startup
23
+ loadEnv();
24
+ function getConfig() {
25
+ return loadConfig(process.cwd());
26
+ }
27
+ function isBundle(path) {
28
+ try {
29
+ const stat = existsSync(path) ? statSync(path) : null;
30
+ if (!stat || !stat.isDirectory())
31
+ return false;
32
+ const entries = readdirSync(path);
33
+ return entries.some((name) => name.endsWith('.md') && name !== 'README.md');
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ // ============================================================================
40
+ // CLI Setup
41
+ // ============================================================================
42
+ const program = new Command();
43
+ program
44
+ .name('marktoflow')
45
+ .description('Universal automation framework with native MCP support')
46
+ .version(VERSION);
47
+ program.addCommand(workerCommand);
48
+ program.addCommand(triggerCommand);
49
+ // ============================================================================
50
+ // Commands
51
+ // ============================================================================
52
+ // --- init ---
53
+ program
54
+ .command('init')
55
+ .description('Initialize a new marktoflow project')
56
+ .option('-f, --force', 'Overwrite existing configuration')
57
+ .action(async (options) => {
58
+ const spinner = ora('Initializing marktoflow project...').start();
59
+ try {
60
+ const configDir = '.marktoflow';
61
+ const workflowsDir = join(configDir, 'workflows');
62
+ const credentialsDir = join(configDir, 'credentials');
63
+ if (existsSync(configDir) && !options.force) {
64
+ spinner.fail('Project already initialized. Use --force to reinitialize.');
65
+ return;
66
+ }
67
+ // Create directories
68
+ mkdirSync(workflowsDir, { recursive: true });
69
+ mkdirSync(credentialsDir, { recursive: true });
70
+ // Create example workflow
71
+ const exampleWorkflow = `---
72
+ workflow:
73
+ id: hello-world
74
+ name: "Hello World"
75
+ version: "1.0.0"
76
+ description: "A simple example workflow"
77
+
78
+ # Uncomment and configure to use Slack:
79
+ # tools:
80
+ # slack:
81
+ # sdk: "@slack/web-api"
82
+ # auth:
83
+ # token: "\${SLACK_BOT_TOKEN}"
84
+
85
+ steps:
86
+ - id: greet
87
+ action: console.log
88
+ inputs:
89
+ message: "Hello from marktoflow!"
90
+ ---
91
+
92
+ # Hello World Workflow
93
+
94
+ This is a simple example workflow.
95
+
96
+ ## Step 1: Greet
97
+
98
+ Outputs a greeting message.
99
+ `;
100
+ writeFileSync(join(workflowsDir, 'hello-world.md'), exampleWorkflow);
101
+ // Create .gitignore for credentials
102
+ writeFileSync(join(credentialsDir, '.gitignore'), '# Ignore all credentials\n*\n!.gitignore\n');
103
+ spinner.succeed('Project initialized successfully!');
104
+ console.log('\n' + chalk.bold('Next steps:'));
105
+ console.log(` 1. Edit ${chalk.cyan('.marktoflow/workflows/hello-world.md')}`);
106
+ console.log(` 2. Run ${chalk.cyan('marktoflow run hello-world.md')}`);
107
+ console.log(` 3. Connect services: ${chalk.cyan('marktoflow connect slack')}`);
108
+ }
109
+ catch (error) {
110
+ spinner.fail(`Initialization failed: ${error}`);
111
+ process.exit(1);
112
+ }
113
+ });
114
+ // --- new ---
115
+ program
116
+ .command('new')
117
+ .description('Create a new workflow from template')
118
+ .option('-o, --output <path>', 'Output file path')
119
+ .option('-t, --template <id>', 'Template ID to use')
120
+ .option('--list-templates', 'List available templates')
121
+ .action(async (options) => {
122
+ if (options.listTemplates) {
123
+ listTemplates();
124
+ return;
125
+ }
126
+ await runWorkflowWizard(options);
127
+ });
128
+ // --- update ---
129
+ program
130
+ .command('update [workflow]')
131
+ .description('Update a workflow using AI coding agents')
132
+ .option('-a, --agent <id>', 'Coding agent to use (opencode, claude-code, cursor, aider)')
133
+ .option('-p, --prompt <text>', 'Update description')
134
+ .option('--list-agents', 'List available coding agents')
135
+ .action(async (workflow, options) => {
136
+ if (options.listAgents) {
137
+ await listAgents();
138
+ return;
139
+ }
140
+ if (!workflow) {
141
+ console.log(chalk.red('\n❌ Error: workflow argument is required\n'));
142
+ console.log('Usage: marktoflow update <workflow> [options]');
143
+ console.log('\nOptions:');
144
+ console.log(' -a, --agent <id> Coding agent to use');
145
+ console.log(' -p, --prompt <text> Update description');
146
+ console.log(' --list-agents List available coding agents');
147
+ process.exit(1);
148
+ }
149
+ await runUpdateWizard({ workflow, ...options });
150
+ });
151
+ // --- run ---
152
+ program
153
+ .command('run <workflow>')
154
+ .description('Run a workflow')
155
+ .option('-i, --input <key=value...>', 'Input parameters')
156
+ .option('-v, --verbose', 'Verbose output')
157
+ .option('--dry-run', 'Parse workflow without executing')
158
+ .action(async (workflowPath, options) => {
159
+ const spinner = ora('Loading workflow...').start();
160
+ try {
161
+ const config = getConfig();
162
+ const workflowsDir = config.workflows?.path ?? '.marktoflow/workflows';
163
+ // Resolve workflow path
164
+ let resolvedPath = workflowPath;
165
+ if (!existsSync(resolvedPath)) {
166
+ resolvedPath = join(workflowsDir, workflowPath);
167
+ }
168
+ if (!existsSync(resolvedPath)) {
169
+ spinner.fail(`Workflow not found: ${workflowPath}`);
170
+ process.exit(1);
171
+ }
172
+ // Parse workflow
173
+ const { workflow, warnings } = await parseFile(resolvedPath);
174
+ if (warnings.length > 0) {
175
+ spinner.warn('Workflow parsed with warnings:');
176
+ warnings.forEach((w) => console.log(chalk.yellow(` - ${w}`)));
177
+ }
178
+ else {
179
+ spinner.succeed(`Loaded: ${workflow.metadata.name}`);
180
+ }
181
+ // Parse inputs
182
+ const inputs = {};
183
+ if (options.input) {
184
+ for (const pair of options.input) {
185
+ const [key, value] = pair.split('=');
186
+ inputs[key] = value;
187
+ }
188
+ }
189
+ // Handle dry-run mode
190
+ if (options.dryRun) {
191
+ const dryRunResult = await executeDryRun(workflow, inputs, {
192
+ verbose: options.verbose,
193
+ showMockData: true,
194
+ showVariables: true,
195
+ });
196
+ displayDryRunSummary(dryRunResult, {
197
+ verbose: options.verbose,
198
+ showMockData: true,
199
+ showVariables: true,
200
+ });
201
+ return;
202
+ }
203
+ // Execute workflow
204
+ spinner.start('Executing workflow...');
205
+ const engine = new WorkflowEngine({}, {
206
+ onStepStart: (step) => {
207
+ if (options.verbose) {
208
+ spinner.text = `Executing: ${step.id}`;
209
+ }
210
+ },
211
+ onStepComplete: (step, result) => {
212
+ if (options.verbose) {
213
+ const icon = result.status === StepStatus.COMPLETED ? '✓' : '✗';
214
+ console.log(` ${icon} ${step.id}: ${result.status}`);
215
+ }
216
+ },
217
+ });
218
+ const registry = new SDKRegistry();
219
+ registerIntegrations(registry);
220
+ registry.registerTools(workflow.tools);
221
+ const result = await engine.execute(workflow, inputs, registry, createSDKStepExecutor());
222
+ if (result.status === WorkflowStatus.COMPLETED) {
223
+ spinner.succeed(`Workflow completed in ${result.duration}ms`);
224
+ }
225
+ else {
226
+ spinner.fail(`Workflow failed: ${result.error}`);
227
+ process.exit(1);
228
+ }
229
+ // Show summary
230
+ console.log('\n' + chalk.bold('Summary:'));
231
+ console.log(` Status: ${result.status}`);
232
+ console.log(` Duration: ${result.duration}ms`);
233
+ console.log(` Steps: ${result.stepResults.length}`);
234
+ const completed = result.stepResults.filter((s) => s.status === StepStatus.COMPLETED).length;
235
+ const failed = result.stepResults.filter((s) => s.status === StepStatus.FAILED).length;
236
+ const skipped = result.stepResults.filter((s) => s.status === StepStatus.SKIPPED).length;
237
+ console.log(` Completed: ${completed}, Failed: ${failed}, Skipped: ${skipped}`);
238
+ }
239
+ catch (error) {
240
+ spinner.fail(`Execution failed: ${error}`);
241
+ process.exit(1);
242
+ }
243
+ });
244
+ // --- debug ---
245
+ program
246
+ .command('debug <workflow>')
247
+ .description('Debug a workflow with step-by-step execution')
248
+ .option('-i, --input <key=value...>', 'Input parameters')
249
+ .option('-b, --breakpoint <stepId...>', 'Set breakpoints at step IDs')
250
+ .option('--auto-start', 'Start without initial prompt')
251
+ .action(async (workflowPath, options) => {
252
+ const spinner = ora('Loading workflow for debugging...').start();
253
+ try {
254
+ const config = getConfig();
255
+ const workflowsDir = config.workflows?.path ?? '.marktoflow/workflows';
256
+ // Resolve workflow path
257
+ let resolvedPath = workflowPath;
258
+ if (!existsSync(resolvedPath)) {
259
+ resolvedPath = join(workflowsDir, workflowPath);
260
+ }
261
+ if (!existsSync(resolvedPath)) {
262
+ spinner.fail(`Workflow not found: ${workflowPath}`);
263
+ process.exit(1);
264
+ }
265
+ // Parse workflow
266
+ const { workflow, warnings } = await parseFile(resolvedPath);
267
+ if (warnings.length > 0) {
268
+ spinner.warn('Workflow parsed with warnings:');
269
+ warnings.forEach((w) => console.log(chalk.yellow(` - ${w}`)));
270
+ }
271
+ else {
272
+ spinner.succeed(`Loaded: ${workflow.metadata.name}`);
273
+ }
274
+ // Parse inputs
275
+ const inputs = {};
276
+ if (options.input) {
277
+ for (const pair of options.input) {
278
+ const [key, value] = pair.split('=');
279
+ inputs[key] = value;
280
+ }
281
+ }
282
+ // Parse breakpoints
283
+ const breakpoints = options.breakpoint ? parseBreakpoints(options.breakpoint) : [];
284
+ // Setup SDK registry and executor
285
+ const registry = new SDKRegistry();
286
+ registerIntegrations(registry);
287
+ registry.registerTools(workflow.tools);
288
+ // Create debugger
289
+ const workflowDebugger = new WorkflowDebugger(workflow, inputs, registry, createSDKStepExecutor(), {
290
+ breakpoints,
291
+ autoStart: options.autoStart,
292
+ });
293
+ // Start debugging session
294
+ await workflowDebugger.debug();
295
+ }
296
+ catch (error) {
297
+ spinner.fail(`Debug session failed: ${error}`);
298
+ process.exit(1);
299
+ }
300
+ });
301
+ // --- workflow list ---
302
+ program
303
+ .command('workflow')
304
+ .description('Workflow management')
305
+ .command('list')
306
+ .description('List available workflows')
307
+ .action(async () => {
308
+ const workflowsDir = getConfig().workflows?.path ?? '.marktoflow/workflows';
309
+ if (!existsSync(workflowsDir)) {
310
+ console.log(chalk.yellow('No workflows found. Run `marktoflow init` first.'));
311
+ return;
312
+ }
313
+ const { readdirSync } = await import('node:fs');
314
+ const files = readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
315
+ if (files.length === 0) {
316
+ console.log(chalk.yellow('No workflows found.'));
317
+ return;
318
+ }
319
+ console.log(chalk.bold('Available Workflows:'));
320
+ for (const file of files) {
321
+ try {
322
+ const { workflow } = await parseFile(join(workflowsDir, file));
323
+ console.log(` ${chalk.cyan(file)}: ${workflow.metadata.name}`);
324
+ }
325
+ catch {
326
+ console.log(` ${chalk.red(file)}: (invalid)`);
327
+ }
328
+ }
329
+ });
330
+ // --- agent ---
331
+ const agentCmd = program.command('agent').description('Agent management');
332
+ agentCmd
333
+ .command('list')
334
+ .description('List available agents')
335
+ .action(() => {
336
+ const capabilitiesPath = join('.marktoflow', 'agents', 'capabilities.yaml');
337
+ const agentsFromFile = [];
338
+ if (existsSync(capabilitiesPath)) {
339
+ const content = readFileSync(capabilitiesPath, 'utf8');
340
+ const data = parseYaml(content);
341
+ agentsFromFile.push(...Object.keys(data?.agents ?? {}));
342
+ }
343
+ const knownAgents = ['claude-code', 'opencode', 'ollama', 'codex', 'gemini-cli'];
344
+ const allAgents = Array.from(new Set([...agentsFromFile, ...knownAgents]));
345
+ console.log(chalk.bold('Available Agents:'));
346
+ for (const agent of allAgents) {
347
+ const status = agentsFromFile.includes(agent)
348
+ ? chalk.green('Registered')
349
+ : chalk.yellow('Not configured');
350
+ console.log(` ${chalk.cyan(agent)}: ${status}`);
351
+ }
352
+ });
353
+ agentCmd
354
+ .command('info <agent>')
355
+ .description('Show agent information')
356
+ .action((agent) => {
357
+ const capabilitiesPath = join('.marktoflow', 'agents', 'capabilities.yaml');
358
+ if (!existsSync(capabilitiesPath)) {
359
+ console.log(chalk.yellow('No capabilities file found. Run `marktoflow init` first.'));
360
+ process.exit(1);
361
+ }
362
+ const content = readFileSync(capabilitiesPath, 'utf8');
363
+ const data = parseYaml(content);
364
+ const info = data?.agents?.[agent];
365
+ if (!info) {
366
+ console.log(chalk.red(`Agent not found: ${agent}`));
367
+ process.exit(1);
368
+ }
369
+ console.log(chalk.bold(agent));
370
+ console.log(` Version: ${info.version ?? 'unknown'}`);
371
+ console.log(` Provider: ${info.provider ?? 'unknown'}`);
372
+ const capabilities = info.capabilities ?? {};
373
+ for (const [key, value] of Object.entries(capabilities)) {
374
+ if (typeof value === 'object' && value) {
375
+ for (const [subKey, subValue] of Object.entries(value)) {
376
+ console.log(` ${key}.${subKey}: ${String(subValue)}`);
377
+ }
378
+ }
379
+ else {
380
+ console.log(` ${key}: ${String(value)}`);
381
+ }
382
+ }
383
+ });
384
+ // --- tools ---
385
+ const toolsCmd = program.command('tools').description('Tool management');
386
+ toolsCmd
387
+ .command('list')
388
+ .description('List available tools')
389
+ .action(() => {
390
+ const registryPath = getConfig().tools?.registryPath ?? join('.marktoflow', 'tools', 'registry.yaml');
391
+ if (!existsSync(registryPath)) {
392
+ console.log(chalk.yellow("No tool registry found. Run 'marktoflow init' first."));
393
+ return;
394
+ }
395
+ const registry = new ToolRegistry(registryPath);
396
+ const tools = registry.listTools();
397
+ if (tools.length === 0) {
398
+ console.log(chalk.yellow('No tools registered.'));
399
+ return;
400
+ }
401
+ console.log(chalk.bold('Registered Tools:'));
402
+ for (const toolName of tools) {
403
+ const definition = registry.getDefinition(toolName);
404
+ const types = definition?.implementations.map((impl) => impl.type).join(', ') ?? '';
405
+ console.log(` ${chalk.cyan(toolName)} ${types ? `(${types})` : ''}`);
406
+ }
407
+ });
408
+ // --- schedule ---
409
+ const scheduleCmd = program.command('schedule').description('Scheduler management');
410
+ scheduleCmd
411
+ .command('list')
412
+ .description('List scheduled workflows')
413
+ .action(() => {
414
+ const scheduler = new Scheduler();
415
+ const jobs = scheduler.listJobs();
416
+ if (jobs.length === 0) {
417
+ console.log(chalk.yellow('No scheduled workflows found.'));
418
+ console.log('Add schedule triggers to your workflows to enable scheduling.');
419
+ return;
420
+ }
421
+ console.log(chalk.bold('Scheduled Workflows:'));
422
+ for (const job of jobs) {
423
+ console.log(` ${chalk.cyan(job.id)} ${job.workflowPath} (${job.schedule})`);
424
+ }
425
+ });
426
+ // --- bundle ---
427
+ const bundleCmd = program.command('bundle').description('Workflow bundle commands');
428
+ bundleCmd
429
+ .command('list [path]')
430
+ .description('List workflow bundles in a directory')
431
+ .action((path = '.') => {
432
+ if (!existsSync(path)) {
433
+ console.log(chalk.red(`Path not found: ${path}`));
434
+ process.exit(1);
435
+ }
436
+ const entries = readdirSync(path, { withFileTypes: true });
437
+ const bundles = [];
438
+ for (const entry of entries) {
439
+ if (!entry.isDirectory())
440
+ continue;
441
+ const fullPath = join(path, entry.name);
442
+ if (isBundle(fullPath))
443
+ bundles.push(fullPath);
444
+ }
445
+ if (bundles.length === 0) {
446
+ console.log(chalk.yellow(`No bundles found in ${path}`));
447
+ return;
448
+ }
449
+ console.log(chalk.bold('Bundles:'));
450
+ for (const bundlePath of bundles) {
451
+ console.log(` ${chalk.cyan(bundlePath)}`);
452
+ }
453
+ });
454
+ bundleCmd
455
+ .command('info <path>')
456
+ .description('Show information about a workflow bundle')
457
+ .action(async (path) => {
458
+ if (!isBundle(path)) {
459
+ console.log(chalk.red(`Not a valid bundle: ${path}`));
460
+ process.exit(1);
461
+ }
462
+ const bundle = new WorkflowBundle(path);
463
+ const workflow = await bundle.loadWorkflow();
464
+ const tools = bundle.loadTools().listTools();
465
+ console.log(chalk.bold(`Bundle: ${bundle.name}`));
466
+ console.log(` Workflow: ${workflow.metadata.name} (${workflow.metadata.id})`);
467
+ console.log(` Steps: ${workflow.steps.length}`);
468
+ console.log(` Tools: ${tools.length ? tools.join(', ') : 'none'}`);
469
+ });
470
+ bundleCmd
471
+ .command('validate <path>')
472
+ .description('Validate a workflow bundle')
473
+ .action(async (path) => {
474
+ if (!isBundle(path)) {
475
+ console.log(chalk.red(`Not a valid bundle: ${path}`));
476
+ process.exit(1);
477
+ }
478
+ try {
479
+ const bundle = new WorkflowBundle(path);
480
+ await bundle.loadWorkflow();
481
+ console.log(chalk.green(`Bundle '${bundle.name}' is valid.`));
482
+ }
483
+ catch (error) {
484
+ console.log(chalk.red(`Bundle validation failed: ${error}`));
485
+ process.exit(1);
486
+ }
487
+ });
488
+ bundleCmd
489
+ .command('run <path>')
490
+ .description('Run a workflow bundle')
491
+ .option('-i, --input <key=value...>', 'Input parameters')
492
+ .action(async (path, options) => {
493
+ if (!isBundle(path)) {
494
+ console.log(chalk.red(`Not a valid bundle: ${path}`));
495
+ process.exit(1);
496
+ }
497
+ const bundle = new WorkflowBundle(path);
498
+ const workflow = await bundle.loadWorkflowWithBundleTools();
499
+ const inputs = {};
500
+ if (options.input) {
501
+ for (const pair of options.input) {
502
+ const [key, value] = pair.split('=');
503
+ inputs[key] = value;
504
+ }
505
+ }
506
+ const engine = new WorkflowEngine();
507
+ const registry = new SDKRegistry();
508
+ registerIntegrations(registry);
509
+ registry.registerTools(workflow.tools);
510
+ const result = await engine.execute(workflow, inputs, registry, createSDKStepExecutor());
511
+ console.log(chalk.bold(`Bundle completed: ${result.status}`));
512
+ });
513
+ // --- template ---
514
+ const templateCmd = program.command('template').description('Workflow template commands');
515
+ templateCmd
516
+ .command('list')
517
+ .description('List workflow templates')
518
+ .action(() => {
519
+ const registry = new TemplateRegistry();
520
+ const templates = registry.list();
521
+ if (!templates.length) {
522
+ console.log(chalk.yellow('No templates found.'));
523
+ return;
524
+ }
525
+ console.log(chalk.bold('Templates:'));
526
+ for (const template of templates) {
527
+ console.log(` ${chalk.cyan(template.id)}: ${template.name}`);
528
+ }
529
+ });
530
+ // --- connect ---
531
+ program
532
+ .command('connect <service>')
533
+ .description('Connect a service (OAuth flow)')
534
+ .option('--client-id <id>', 'OAuth client ID')
535
+ .option('--client-secret <secret>', 'OAuth client secret')
536
+ .option('--tenant-id <tenant>', 'Microsoft tenant ID (for Outlook)')
537
+ .action(async (service, options) => {
538
+ const serviceLower = service.toLowerCase();
539
+ console.log(chalk.bold(`Connecting ${service}...`));
540
+ // Services that support OAuth flow
541
+ if (serviceLower === 'gmail') {
542
+ const clientId = options.clientId ?? process.env.GOOGLE_CLIENT_ID;
543
+ const clientSecret = options.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET;
544
+ if (!clientId || !clientSecret) {
545
+ console.log(chalk.yellow('\nGmail OAuth requires client credentials.'));
546
+ console.log('\nTo connect Gmail:');
547
+ console.log(' 1. Go to https://console.cloud.google.com/');
548
+ console.log(' 2. Create OAuth 2.0 credentials (Desktop app type)');
549
+ console.log(' 3. Run: marktoflow connect gmail --client-id YOUR_ID --client-secret YOUR_SECRET');
550
+ console.log('\nOr set environment variables:');
551
+ console.log(' export GOOGLE_CLIENT_ID="your-client-id"');
552
+ console.log(' export GOOGLE_CLIENT_SECRET="your-client-secret"');
553
+ return;
554
+ }
555
+ try {
556
+ const { runGmailOAuth } = await import('./oauth.js');
557
+ const tokens = await runGmailOAuth({ clientId, clientSecret });
558
+ console.log(chalk.green('\nGmail connected successfully!'));
559
+ console.log(chalk.dim(`Access token expires: ${tokens.expires_at ? new Date(tokens.expires_at).toISOString() : 'unknown'}`));
560
+ console.log('\nYou can now use Gmail in your workflows:');
561
+ console.log(chalk.cyan(` tools:
562
+ gmail:
563
+ sdk: "googleapis"
564
+ auth:
565
+ client_id: "\${GOOGLE_CLIENT_ID}"
566
+ client_secret: "\${GOOGLE_CLIENT_SECRET}"
567
+ redirect_uri: "http://localhost:8484/callback"
568
+ refresh_token: "\${GMAIL_REFRESH_TOKEN}"`));
569
+ }
570
+ catch (error) {
571
+ console.log(chalk.red(`\nOAuth failed: ${error}`));
572
+ process.exit(1);
573
+ }
574
+ return;
575
+ }
576
+ if (serviceLower === 'outlook' || serviceLower === 'microsoft') {
577
+ const clientId = options.clientId ?? process.env.MICROSOFT_CLIENT_ID;
578
+ const clientSecret = options.clientSecret ?? process.env.MICROSOFT_CLIENT_SECRET;
579
+ const tenantId = options.tenantId ?? process.env.MICROSOFT_TENANT_ID;
580
+ if (!clientId) {
581
+ console.log(chalk.yellow('\nOutlook OAuth requires a client ID.'));
582
+ console.log('\nTo connect Outlook/Microsoft Graph:');
583
+ console.log(' 1. Go to https://portal.azure.com/');
584
+ console.log(' 2. Register an application in Azure AD');
585
+ console.log(' 3. Add redirect URI: http://localhost:8484/callback');
586
+ console.log(' 4. Grant Mail.Read, Mail.Send, Calendars.ReadWrite permissions');
587
+ console.log(' 5. Run: marktoflow connect outlook --client-id YOUR_ID');
588
+ console.log('\nOr set environment variables:');
589
+ console.log(' export MICROSOFT_CLIENT_ID="your-client-id"');
590
+ console.log(' export MICROSOFT_CLIENT_SECRET="your-client-secret" # optional');
591
+ console.log(' export MICROSOFT_TENANT_ID="common" # or your tenant ID');
592
+ return;
593
+ }
594
+ try {
595
+ const { runOutlookOAuth } = await import('./oauth.js');
596
+ const tokens = await runOutlookOAuth({ clientId, clientSecret, tenantId });
597
+ console.log(chalk.green('\nOutlook connected successfully!'));
598
+ console.log(chalk.dim(`Access token expires: ${tokens.expires_at ? new Date(tokens.expires_at).toISOString() : 'unknown'}`));
599
+ console.log('\nYou can now use Outlook in your workflows:');
600
+ console.log(chalk.cyan(` tools:
601
+ outlook:
602
+ sdk: "@microsoft/microsoft-graph-client"
603
+ auth:
604
+ token: "\${OUTLOOK_ACCESS_TOKEN}"`));
605
+ }
606
+ catch (error) {
607
+ console.log(chalk.red(`\nOAuth failed: ${error}`));
608
+ process.exit(1);
609
+ }
610
+ return;
611
+ }
612
+ // Other services - show manual setup instructions
613
+ console.log('\nManual setup required. Set environment variables:');
614
+ switch (serviceLower) {
615
+ case 'slack':
616
+ console.log(` export SLACK_BOT_TOKEN="xoxb-your-token"`);
617
+ console.log(` export SLACK_APP_TOKEN="xapp-your-token"`);
618
+ console.log(chalk.dim('\n Get tokens from https://api.slack.com/apps'));
619
+ break;
620
+ case 'github':
621
+ console.log(` export GITHUB_TOKEN="ghp_your-token"`);
622
+ console.log(chalk.dim('\n Create token at https://github.com/settings/tokens'));
623
+ break;
624
+ case 'jira':
625
+ console.log(` export JIRA_HOST="https://your-domain.atlassian.net"`);
626
+ console.log(` export JIRA_EMAIL="your-email@example.com"`);
627
+ console.log(` export JIRA_API_TOKEN="your-api-token"`);
628
+ console.log(chalk.dim('\n Create token at https://id.atlassian.com/manage-profile/security/api-tokens'));
629
+ break;
630
+ case 'confluence':
631
+ console.log(` export CONFLUENCE_HOST="https://your-domain.atlassian.net"`);
632
+ console.log(` export CONFLUENCE_EMAIL="your-email@example.com"`);
633
+ console.log(` export CONFLUENCE_API_TOKEN="your-api-token"`);
634
+ console.log(chalk.dim('\n Create token at https://id.atlassian.com/manage-profile/security/api-tokens'));
635
+ break;
636
+ case 'linear':
637
+ console.log(` export LINEAR_API_KEY="lin_api_your-key"`);
638
+ console.log(chalk.dim('\n Create key at https://linear.app/settings/api'));
639
+ break;
640
+ case 'notion':
641
+ console.log(` export NOTION_TOKEN="secret_your-token"`);
642
+ console.log(chalk.dim('\n Create integration at https://www.notion.so/my-integrations'));
643
+ break;
644
+ case 'discord':
645
+ console.log(` export DISCORD_BOT_TOKEN="your-bot-token"`);
646
+ console.log(chalk.dim('\n Create bot at https://discord.com/developers/applications'));
647
+ break;
648
+ case 'airtable':
649
+ console.log(` export AIRTABLE_TOKEN="pat_your-token"`);
650
+ console.log(` export AIRTABLE_BASE_ID="appXXXXX" # optional default base`);
651
+ console.log(chalk.dim('\n Create token at https://airtable.com/create/tokens'));
652
+ break;
653
+ case 'anthropic':
654
+ console.log(` export ANTHROPIC_API_KEY="sk-ant-your-key"`);
655
+ console.log(chalk.dim('\n Get key at https://console.anthropic.com/'));
656
+ break;
657
+ case 'openai':
658
+ console.log(` export OPENAI_API_KEY="sk-your-key"`);
659
+ console.log(chalk.dim('\n Get key at https://platform.openai.com/api-keys'));
660
+ break;
661
+ default:
662
+ console.log(` See documentation for ${service} configuration.`);
663
+ console.log('\n' + chalk.bold('Available services:'));
664
+ console.log(' Communication: slack, discord');
665
+ console.log(' Email: gmail, outlook');
666
+ console.log(' Project management: jira, linear');
667
+ console.log(' Documentation: notion, confluence');
668
+ console.log(' Developer: github');
669
+ console.log(' Data: airtable');
670
+ console.log(' AI: anthropic, openai');
671
+ }
672
+ });
673
+ // --- doctor ---
674
+ program
675
+ .command('doctor')
676
+ .description('Check environment and configuration')
677
+ .action(async () => {
678
+ console.log(chalk.bold('marktoflow Doctor\n'));
679
+ // Node version
680
+ const nodeVersion = process.version;
681
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
682
+ if (nodeMajor >= 20) {
683
+ console.log(chalk.green('✓') + ` Node.js ${nodeVersion}`);
684
+ }
685
+ else {
686
+ console.log(chalk.red('✗') + ` Node.js ${nodeVersion} (requires >=20)`);
687
+ }
688
+ // Project initialized
689
+ if (existsSync('.marktoflow')) {
690
+ console.log(chalk.green('✓') + ' Project initialized');
691
+ // Count workflows
692
+ const workflowsDir = '.marktoflow/workflows';
693
+ if (existsSync(workflowsDir)) {
694
+ const { readdirSync } = await import('node:fs');
695
+ const workflows = readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
696
+ console.log(chalk.green('✓') + ` ${workflows.length} workflow(s) found`);
697
+ }
698
+ }
699
+ else {
700
+ console.log(chalk.yellow('○') + ' Project not initialized');
701
+ }
702
+ // Check for common environment variables
703
+ const envChecks = [
704
+ // Communication
705
+ ['SLACK_BOT_TOKEN', 'Slack'],
706
+ ['DISCORD_BOT_TOKEN', 'Discord'],
707
+ // Email
708
+ ['GOOGLE_CLIENT_ID', 'Gmail'],
709
+ ['MICROSOFT_CLIENT_ID', 'Outlook'],
710
+ // Project Management
711
+ ['JIRA_API_TOKEN', 'Jira'],
712
+ ['LINEAR_API_KEY', 'Linear'],
713
+ // Documentation
714
+ ['NOTION_TOKEN', 'Notion'],
715
+ ['CONFLUENCE_API_TOKEN', 'Confluence'],
716
+ // Developer
717
+ ['GITHUB_TOKEN', 'GitHub'],
718
+ // Data
719
+ ['AIRTABLE_TOKEN', 'Airtable'],
720
+ // AI
721
+ ['ANTHROPIC_API_KEY', 'Anthropic'],
722
+ ['OPENAI_API_KEY', 'OpenAI'],
723
+ ];
724
+ console.log('\n' + chalk.bold('Services:'));
725
+ let configuredCount = 0;
726
+ for (const [envVar, name] of envChecks) {
727
+ if (process.env[envVar]) {
728
+ console.log(chalk.green('✓') + ` ${name} configured`);
729
+ configuredCount++;
730
+ }
731
+ else {
732
+ console.log(chalk.dim('○') + ` ${name} not configured`);
733
+ }
734
+ }
735
+ if (configuredCount === 0) {
736
+ console.log(chalk.yellow('\n Run `marktoflow connect <service>` to set up integrations'));
737
+ }
738
+ });
739
+ // --- gui ---
740
+ program
741
+ .command('gui')
742
+ .description('Launch visual workflow designer')
743
+ .option('-p, --port <port>', 'Server port', '3001')
744
+ .option('-o, --open', 'Open browser automatically')
745
+ .option('-w, --workflow <path>', 'Open specific workflow')
746
+ .option('-d, --dir <path>', 'Workflow directory', '.')
747
+ .action(async (options) => {
748
+ const spinner = ora('Starting GUI server...').start();
749
+ try {
750
+ // Check if @marktoflow/gui is available
751
+ let guiModule;
752
+ let guiPackagePath;
753
+ try {
754
+ guiModule = await import('@marktoflow/gui');
755
+ // Find the GUI package location
756
+ const { createRequire } = await import('node:module');
757
+ const require = createRequire(import.meta.url);
758
+ guiPackagePath = require.resolve('@marktoflow/gui');
759
+ }
760
+ catch {
761
+ spinner.fail('@marktoflow/gui package not found');
762
+ console.log(chalk.yellow('\nTo use the GUI, install the gui package:'));
763
+ console.log(chalk.cyan(' npm install @marktoflow/gui@alpha'));
764
+ console.log('\nOr run from the monorepo:');
765
+ console.log(chalk.cyan(' pnpm --filter @marktoflow/gui dev'));
766
+ process.exit(1);
767
+ }
768
+ spinner.succeed(`GUI server starting on http://localhost:${options.port}`);
769
+ // Open browser if requested
770
+ if (options.open) {
771
+ const url = `http://localhost:${options.port}`;
772
+ const { exec } = await import('node:child_process');
773
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
774
+ exec(`${openCmd} ${url}`);
775
+ }
776
+ console.log('\n' + chalk.bold('Marktoflow GUI'));
777
+ console.log(` Server: ${chalk.cyan(`http://localhost:${options.port}`)}`);
778
+ console.log(` Workflows: ${chalk.cyan(options.dir)}`);
779
+ console.log('\n Press ' + chalk.bold('Ctrl+C') + ' to stop\n');
780
+ // Find the static files directory
781
+ const { dirname, join } = await import('node:path');
782
+ const guiPackageDir = dirname(dirname(guiPackagePath));
783
+ const staticDir = join(guiPackageDir, 'dist', 'client');
784
+ // The GUI package will handle the server
785
+ if (guiModule.startServer) {
786
+ await guiModule.startServer({
787
+ port: parseInt(options.port, 10),
788
+ workflowDir: options.dir,
789
+ staticDir: staticDir,
790
+ });
791
+ }
792
+ }
793
+ catch (error) {
794
+ spinner.fail(`Failed to start GUI: ${error}`);
795
+ process.exit(1);
796
+ }
797
+ });
798
+ // --- version ---
799
+ program
800
+ .command('version')
801
+ .description('Show version information')
802
+ .action(() => {
803
+ console.log(`marktoflow v${VERSION}`);
804
+ });
805
+ // ============================================================================
806
+ // Parse and Execute
807
+ // ============================================================================
808
+ program.parse();
809
+ //# sourceMappingURL=index.js.map