@marktoflow/cli 2.0.0-alpha.3

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