@marktoflow/cli 2.0.0-alpha.7 → 2.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.
package/dist/index.js CHANGED
@@ -2,23 +2,27 @@
2
2
  /**
3
3
  * marktoflow CLI
4
4
  *
5
- * Universal automation framework with native MCP support.
5
+ * Agent automation framework with native MCP support.
6
6
  */
7
7
  import { Command } from 'commander';
8
8
  import chalk from 'chalk';
9
9
  import ora from 'ora';
10
10
  import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, readFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
- import { parseFile, WorkflowEngine, SDKRegistry, createSDKStepExecutor, StepStatus, WorkflowStatus, loadEnv, ToolRegistry, WorkflowBundle, Scheduler, TemplateRegistry, loadConfig, } from '@marktoflow/core';
12
+ import { parseFile, WorkflowEngine, SDKRegistry, createSDKStepExecutor, StepStatus, WorkflowStatus, loadEnv, ToolRegistry, WorkflowBundle, Scheduler, TemplateRegistry, loadConfig, createCredentialManager, getAvailableBackends, EncryptionBackend, StateStore, } from '@marktoflow/core';
13
13
  import { registerIntegrations } from '@marktoflow/integrations';
14
14
  import { workerCommand } from './worker.js';
15
15
  import { triggerCommand } from './trigger.js';
16
+ import { serveCommand } from './serve.js';
16
17
  import { runWorkflowWizard, listTemplates } from './commands/new.js';
17
18
  import { runUpdateWizard, listAgents } from './commands/update.js';
18
19
  import { parse as parseYaml } from 'yaml';
19
20
  import { executeDryRun, displayDryRunSummary } from './commands/dry-run.js';
20
21
  import { WorkflowDebugger, parseBreakpoints } from './commands/debug.js';
21
- const VERSION = '2.0.0-alpha.7';
22
+ import { executeTestConnection } from './commands/test-connection.js';
23
+ import { executeHistory, executeHistoryDetail, executeReplay } from './commands/history.js';
24
+ import { parseInputPairs, debugLogInputs, validateAndApplyDefaults, printMissingInputsError, overrideAgentInWorkflow, debugLogAgentOverride, overrideModelInWorkflow, } from './utils/index.js';
25
+ const VERSION = '2.0.0-alpha.15';
22
26
  // Load environment variables from .env files on CLI startup
23
27
  loadEnv();
24
28
  function getConfig() {
@@ -36,16 +40,62 @@ function isBundle(path) {
36
40
  return false;
37
41
  }
38
42
  }
43
+ /**
44
+ * Map agent provider aliases to SDK names
45
+ */
46
+ function getAgentSDKName(provider) {
47
+ const providerMap = {
48
+ 'claude': 'claude-code',
49
+ 'claude-code': 'claude-code',
50
+ 'claude-agent': 'claude-agent',
51
+ 'copilot': 'github-copilot',
52
+ 'github-copilot': 'github-copilot',
53
+ 'opencode': 'opencode',
54
+ 'ollama': 'ollama',
55
+ 'codex': 'codex',
56
+ };
57
+ const normalized = provider.toLowerCase();
58
+ const sdkName = providerMap[normalized];
59
+ if (!sdkName) {
60
+ throw new Error(`Unknown agent provider: ${provider}. Available: ${Object.keys(providerMap).join(', ')}`);
61
+ }
62
+ return sdkName;
63
+ }
64
+ /**
65
+ * Get default auth configuration for an agent provider
66
+ */
67
+ function getAgentAuthConfig(sdkName) {
68
+ const authConfigs = {
69
+ 'claude-code': {
70
+ api_key: '${ANTHROPIC_API_KEY}',
71
+ },
72
+ 'claude-agent': {
73
+ api_key: '${ANTHROPIC_API_KEY}',
74
+ },
75
+ 'github-copilot': {
76
+ token: '${GITHUB_TOKEN}',
77
+ },
78
+ 'opencode': {},
79
+ 'ollama': {
80
+ base_url: '${OLLAMA_BASE_URL:-http://localhost:11434}',
81
+ },
82
+ 'codex': {
83
+ api_key: '${OPENAI_API_KEY}',
84
+ },
85
+ };
86
+ return authConfigs[sdkName] || {};
87
+ }
39
88
  // ============================================================================
40
89
  // CLI Setup
41
90
  // ============================================================================
42
91
  const program = new Command();
43
92
  program
44
93
  .name('marktoflow')
45
- .description('Universal automation framework with native MCP support')
94
+ .description('Agent automation framework with native MCP support')
46
95
  .version(VERSION);
47
96
  program.addCommand(workerCommand);
48
97
  program.addCommand(triggerCommand);
98
+ program.addCommand(serveCommand);
49
99
  // ============================================================================
50
100
  // Commands
51
101
  // ============================================================================
@@ -84,7 +134,7 @@ workflow:
84
134
 
85
135
  steps:
86
136
  - id: greet
87
- action: console.log
137
+ action: core.log
88
138
  inputs:
89
139
  message: "Hello from marktoflow!"
90
140
  ---
@@ -154,9 +204,13 @@ program
154
204
  .description('Run a workflow')
155
205
  .option('-i, --input <key=value...>', 'Input parameters')
156
206
  .option('-v, --verbose', 'Verbose output')
207
+ .option('-d, --debug', 'Debug mode with detailed output (includes stack traces)')
208
+ .option('-a, --agent <provider>', 'AI agent provider (claude-code, claude-agent, github-copilot, opencode, ollama, codex)')
209
+ .option('-m, --model <name>', 'Model name to use (e.g., claude-sonnet-4, gpt-4, etc.)')
157
210
  .option('--dry-run', 'Parse workflow without executing')
158
211
  .action(async (workflowPath, options) => {
159
212
  const spinner = ora('Loading workflow...').start();
213
+ let stateStore;
160
214
  try {
161
215
  const config = getConfig();
162
216
  const workflowsDir = config.workflows?.path ?? '.marktoflow/workflows';
@@ -178,12 +232,56 @@ program
178
232
  else {
179
233
  spinner.succeed(`Loaded: ${workflow.metadata.name}`);
180
234
  }
235
+ // Debug: Show workflow details
236
+ if (options.debug) {
237
+ console.log(chalk.gray('\n🐛 Debug: Workflow Details'));
238
+ console.log(chalk.gray(` ID: ${workflow.metadata.id}`));
239
+ console.log(chalk.gray(` Version: ${workflow.metadata.version}`));
240
+ console.log(chalk.gray(` Steps: ${workflow.steps.length}`));
241
+ console.log(chalk.gray(` Tools: ${Object.keys(workflow.tools).join(', ') || 'none'}`));
242
+ console.log(chalk.gray(` Inputs Required: ${Object.keys(workflow.inputs || {}).join(', ') || 'none'}`));
243
+ }
181
244
  // 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;
245
+ const parsedInputs = parseInputPairs(options.input);
246
+ // Debug: Show parsed inputs
247
+ if (options.debug) {
248
+ debugLogInputs(parsedInputs);
249
+ }
250
+ // Validate required inputs and apply defaults
251
+ const validation = validateAndApplyDefaults(workflow, parsedInputs, { debug: options.debug });
252
+ if (!validation.valid) {
253
+ spinner.fail('Missing required inputs');
254
+ printMissingInputsError(workflow, validation.missingInputs, 'run', workflowPath);
255
+ process.exit(1);
256
+ }
257
+ const inputs = validation.inputs;
258
+ // Override AI agent if specified
259
+ if (options.agent) {
260
+ const sdkName = getAgentSDKName(options.agent);
261
+ const authConfig = getAgentAuthConfig(sdkName);
262
+ const result = overrideAgentInWorkflow(workflow, sdkName, authConfig, {
263
+ verbose: options.verbose,
264
+ debug: options.debug,
265
+ });
266
+ if (options.debug) {
267
+ debugLogAgentOverride(options.agent, sdkName, result.replacedCount, authConfig);
268
+ }
269
+ }
270
+ // Override model if specified
271
+ if (options.model) {
272
+ const result = overrideModelInWorkflow(workflow, options.model);
273
+ if (options.verbose || options.debug) {
274
+ if (result.overrideCount > 0) {
275
+ console.log(chalk.cyan(` Set model '${options.model}' for ${result.overrideCount} AI tool(s)`));
276
+ }
277
+ }
278
+ if (options.debug) {
279
+ console.log(chalk.gray('\n🐛 Debug: Model Override'));
280
+ console.log(chalk.gray(` Model: ${options.model}`));
281
+ console.log(chalk.gray(` Applied to ${result.overrideCount} AI tool(s)`));
282
+ }
283
+ if (result.overrideCount === 0 && (options.verbose || options.debug)) {
284
+ console.log(chalk.yellow(` Warning: --model specified but no AI tools found in workflow`));
187
285
  }
188
286
  }
189
287
  // Handle dry-run mode
@@ -202,28 +300,172 @@ program
202
300
  }
203
301
  // Execute workflow
204
302
  spinner.start('Executing workflow...');
303
+ // Debug: Show execution start
304
+ if (options.debug) {
305
+ console.log(chalk.gray('\n🐛 Debug: Starting Workflow Execution'));
306
+ console.log(chalk.gray(` Workflow: ${workflow.metadata.name}`));
307
+ console.log(chalk.gray(` Steps to execute: ${workflow.steps.length}`));
308
+ }
309
+ // Track which steps we've logged to avoid duplicate output on retries
310
+ const loggedSteps = new Set();
311
+ // Create StateStore for execution history
312
+ const stateDir = join(process.cwd(), '.marktoflow');
313
+ mkdirSync(stateDir, { recursive: true });
314
+ stateStore = new StateStore(join(stateDir, 'state.db'));
205
315
  const engine = new WorkflowEngine({}, {
206
316
  onStepStart: (step) => {
207
- if (options.verbose) {
317
+ if (options.verbose || options.debug) {
208
318
  spinner.text = `Executing: ${step.id}`;
209
319
  }
320
+ // Only log step start once (not on retries)
321
+ if (options.debug && !loggedSteps.has(step.id)) {
322
+ console.log(chalk.gray(`\n🐛 Debug: Step Start - ${step.id}`));
323
+ console.log(chalk.gray(` Action: ${step.action || 'N/A'}`));
324
+ if (step.inputs) {
325
+ console.log(chalk.gray(` Inputs: ${JSON.stringify(step.inputs, null, 2).split('\n').join('\n ')}`));
326
+ }
327
+ loggedSteps.add(step.id);
328
+ }
210
329
  },
211
330
  onStepComplete: (step, result) => {
212
- if (options.verbose) {
331
+ if (options.verbose || options.debug) {
213
332
  const icon = result.status === StepStatus.COMPLETED ? '✓' : '✗';
214
333
  console.log(` ${icon} ${step.id}: ${result.status}`);
215
334
  }
335
+ if (options.debug) {
336
+ console.log(chalk.gray(`\n🐛 Debug: Step Complete - ${step.id}`));
337
+ console.log(chalk.gray(` Status: ${result.status}`));
338
+ console.log(chalk.gray(` Duration: ${result.duration}ms`));
339
+ // Show output variable name if set
340
+ if (step.outputVariable) {
341
+ console.log(chalk.gray(` Output Variable: ${step.outputVariable}`));
342
+ }
343
+ // Show full output in debug mode (not truncated)
344
+ if (result.output !== undefined) {
345
+ let outputStr;
346
+ if (typeof result.output === 'string') {
347
+ outputStr = result.output;
348
+ }
349
+ else {
350
+ outputStr = JSON.stringify(result.output, null, 2);
351
+ }
352
+ // Split into lines and indent each line
353
+ const lines = outputStr.split('\n');
354
+ if (lines.length > 50) {
355
+ // If output is very large (>50 lines), show first 40 and last 5 lines
356
+ console.log(chalk.gray(` Output (${lines.length} lines):`));
357
+ lines.slice(0, 40).forEach(line => console.log(chalk.gray(` ${line}`)));
358
+ console.log(chalk.gray(` ... (${lines.length - 45} lines omitted) ...`));
359
+ lines.slice(-5).forEach(line => console.log(chalk.gray(` ${line}`)));
360
+ }
361
+ else {
362
+ console.log(chalk.gray(` Output:`));
363
+ lines.forEach(line => console.log(chalk.gray(` ${line}`)));
364
+ }
365
+ }
366
+ if (result.error) {
367
+ console.log(chalk.red(` Error: ${result.error}`));
368
+ }
369
+ }
216
370
  },
217
- });
371
+ }, stateStore);
372
+ // Set workflow path for execution history
373
+ engine.workflowPath = resolvedPath;
218
374
  const registry = new SDKRegistry();
219
375
  registerIntegrations(registry);
220
376
  registry.registerTools(workflow.tools);
377
+ // Debug: Show registered tools
378
+ if (options.debug) {
379
+ console.log(chalk.gray('\n🐛 Debug: SDK Registry'));
380
+ console.log(chalk.gray(` Registered tools: ${Object.keys(workflow.tools).join(', ')}`));
381
+ }
221
382
  const result = await engine.execute(workflow, inputs, registry, createSDKStepExecutor());
222
383
  if (result.status === WorkflowStatus.COMPLETED) {
223
384
  spinner.succeed(`Workflow completed in ${result.duration}ms`);
224
385
  }
225
386
  else {
226
387
  spinner.fail(`Workflow failed: ${result.error}`);
388
+ // Debug: Show detailed error information
389
+ if (options.debug) {
390
+ console.log(chalk.red('\n🐛 Debug: Failure Details'));
391
+ console.log(chalk.red(` Error: ${result.error}`));
392
+ // Find the failed step
393
+ const failedStep = result.stepResults.find(s => s.status === StepStatus.FAILED);
394
+ if (failedStep) {
395
+ console.log(chalk.red(` Failed Step: ${failedStep.stepId}`));
396
+ console.log(chalk.red(` Step Duration: ${failedStep.duration}ms`));
397
+ if (failedStep.error) {
398
+ console.log(chalk.red(` Step Error: ${failedStep.error}`));
399
+ // Extract detailed error information
400
+ const errorObj = typeof failedStep.error === 'object' ? failedStep.error : null;
401
+ if (errorObj) {
402
+ // HTTP error details (Axios, fetch, etc.)
403
+ if (errorObj.response) {
404
+ console.log(chalk.red('\n HTTP Error Details:'));
405
+ console.log(chalk.red(` Status: ${errorObj.response.status} ${errorObj.response.statusText || ''}`));
406
+ if (errorObj.config?.url) {
407
+ console.log(chalk.red(` URL: ${errorObj.config.method?.toUpperCase() || 'GET'} ${errorObj.config.url}`));
408
+ }
409
+ if (errorObj.response.data) {
410
+ console.log(chalk.red(` Response Body:`));
411
+ try {
412
+ const responseStr = typeof errorObj.response.data === 'string'
413
+ ? errorObj.response.data
414
+ : JSON.stringify(errorObj.response.data, null, 2);
415
+ const lines = responseStr.split('\n').slice(0, 20); // First 20 lines
416
+ lines.forEach((line) => console.log(chalk.red(` ${line}`)));
417
+ if (responseStr.split('\n').length > 20) {
418
+ console.log(chalk.red(` ... (truncated)`));
419
+ }
420
+ }
421
+ catch {
422
+ console.log(chalk.red(` [Unable to serialize response]`));
423
+ }
424
+ }
425
+ if (errorObj.response.headers) {
426
+ console.log(chalk.red(` Response Headers:`));
427
+ const headers = errorObj.response.headers;
428
+ Object.keys(headers).slice(0, 10).forEach((key) => {
429
+ console.log(chalk.red(` ${key}: ${headers[key]}`));
430
+ });
431
+ }
432
+ }
433
+ // Request details
434
+ if (errorObj.config && !errorObj.response) {
435
+ console.log(chalk.red('\n Request Details:'));
436
+ if (errorObj.config.url) {
437
+ console.log(chalk.red(` URL: ${errorObj.config.method?.toUpperCase() || 'GET'} ${errorObj.config.url}`));
438
+ }
439
+ if (errorObj.config.baseURL) {
440
+ console.log(chalk.red(` Base URL: ${errorObj.config.baseURL}`));
441
+ }
442
+ if (errorObj.code) {
443
+ console.log(chalk.red(` Error Code: ${errorObj.code}`));
444
+ }
445
+ }
446
+ // Stack trace
447
+ if (errorObj.stack) {
448
+ console.log(chalk.red('\n Stack Trace:'));
449
+ const stack = errorObj.stack.split('\n').slice(0, 15); // First 15 lines
450
+ stack.forEach((line) => console.log(chalk.red(` ${line}`)));
451
+ if (errorObj.stack.split('\n').length > 15) {
452
+ console.log(chalk.red(` ... (truncated)`));
453
+ }
454
+ }
455
+ }
456
+ }
457
+ if (failedStep.output) {
458
+ console.log(chalk.red(` Output: ${JSON.stringify(failedStep.output, null, 2)}`));
459
+ }
460
+ }
461
+ // Show context from previous steps
462
+ console.log(chalk.yellow('\n🐛 Debug: Execution Context'));
463
+ console.log(chalk.yellow(` Total steps executed: ${result.stepResults.length}`));
464
+ console.log(chalk.yellow(` Steps before failure:`));
465
+ result.stepResults.slice(0, -1).forEach(stepResult => {
466
+ console.log(chalk.yellow(` - ${stepResult.stepId}: ${stepResult.status}`));
467
+ });
468
+ }
227
469
  process.exit(1);
228
470
  }
229
471
  // Show summary
@@ -235,9 +477,42 @@ program
235
477
  const failed = result.stepResults.filter((s) => s.status === StepStatus.FAILED).length;
236
478
  const skipped = result.stepResults.filter((s) => s.status === StepStatus.SKIPPED).length;
237
479
  console.log(` Completed: ${completed}, Failed: ${failed}, Skipped: ${skipped}`);
480
+ // Close StateStore and exit successfully to avoid hanging due to open SDK connections
481
+ stateStore.close();
482
+ process.exit(0);
238
483
  }
239
484
  catch (error) {
240
485
  spinner.fail(`Execution failed: ${error}`);
486
+ // Debug: Show full error details with stack trace
487
+ if (options.debug) {
488
+ console.log(chalk.red('\n🐛 Debug: Unhandled Error Details'));
489
+ console.log(chalk.red(` Error Type: ${error instanceof Error ? error.constructor.name : typeof error}`));
490
+ console.log(chalk.red(` Error Message: ${error instanceof Error ? error.message : String(error)}`));
491
+ if (error instanceof Error && error.stack) {
492
+ console.log(chalk.red('\n Stack Trace:'));
493
+ error.stack.split('\n').forEach(line => {
494
+ console.log(chalk.red(` ${line}`));
495
+ });
496
+ }
497
+ // Show error properties if available
498
+ if (error && typeof error === 'object') {
499
+ const errorObj = error;
500
+ const keys = Object.keys(errorObj).filter(k => k !== 'stack' && k !== 'message');
501
+ if (keys.length > 0) {
502
+ console.log(chalk.red('\n Additional Error Properties:'));
503
+ keys.forEach(key => {
504
+ try {
505
+ console.log(chalk.red(` ${key}: ${JSON.stringify(errorObj[key], null, 2)}`));
506
+ }
507
+ catch {
508
+ console.log(chalk.red(` ${key}: [Unable to serialize]`));
509
+ }
510
+ });
511
+ }
512
+ }
513
+ }
514
+ // Close StateStore if it was created
515
+ stateStore?.close();
241
516
  process.exit(1);
242
517
  }
243
518
  });
@@ -247,6 +522,8 @@ program
247
522
  .description('Debug a workflow with step-by-step execution')
248
523
  .option('-i, --input <key=value...>', 'Input parameters')
249
524
  .option('-b, --breakpoint <stepId...>', 'Set breakpoints at step IDs')
525
+ .option('-a, --agent <provider>', 'AI agent provider (claude-code, claude-agent, github-copilot, opencode, ollama, codex)')
526
+ .option('-m, --model <name>', 'Model name to use (e.g., claude-sonnet-4, gpt-4, etc.)')
250
527
  .option('--auto-start', 'Start without initial prompt')
251
528
  .action(async (workflowPath, options) => {
252
529
  const spinner = ora('Loading workflow for debugging...').start();
@@ -271,13 +548,24 @@ program
271
548
  else {
272
549
  spinner.succeed(`Loaded: ${workflow.metadata.name}`);
273
550
  }
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
- }
551
+ // Parse and validate inputs
552
+ const parsedInputs = parseInputPairs(options.input);
553
+ const validation = validateAndApplyDefaults(workflow, parsedInputs);
554
+ if (!validation.valid) {
555
+ spinner.fail('Missing required inputs');
556
+ printMissingInputsError(workflow, validation.missingInputs, 'debug', workflowPath);
557
+ process.exit(1);
558
+ }
559
+ const inputs = validation.inputs;
560
+ // Override AI agent if specified
561
+ if (options.agent) {
562
+ const sdkName = getAgentSDKName(options.agent);
563
+ const authConfig = getAgentAuthConfig(sdkName);
564
+ overrideAgentInWorkflow(workflow, sdkName, authConfig);
565
+ }
566
+ // Override model if specified
567
+ if (options.model) {
568
+ overrideModelInWorkflow(workflow, options.model);
281
569
  }
282
570
  // Parse breakpoints
283
571
  const breakpoints = options.breakpoint ? parseBreakpoints(options.breakpoint) : [];
@@ -405,6 +693,107 @@ toolsCmd
405
693
  console.log(` ${chalk.cyan(toolName)} ${types ? `(${types})` : ''}`);
406
694
  }
407
695
  });
696
+ // --- credentials ---
697
+ const credentialsCmd = program.command('credentials').description('Credential management');
698
+ credentialsCmd
699
+ .command('list')
700
+ .description('List stored credentials')
701
+ .option('--state-dir <path>', 'State directory', join('.marktoflow', 'credentials'))
702
+ .option('--backend <backend>', 'Encryption backend (aes-256-gcm, fernet, age, gpg)')
703
+ .option('--tag <tag>', 'Filter by tag')
704
+ .option('--show-expired', 'Include expired credentials')
705
+ .action((options) => {
706
+ try {
707
+ const stateDir = options.stateDir;
708
+ const backend = options.backend ?? undefined;
709
+ const manager = createCredentialManager({ stateDir, backend });
710
+ const credentials = manager.list(options.tag, options.showExpired);
711
+ if (credentials.length === 0) {
712
+ console.log(chalk.yellow('No credentials found.'));
713
+ return;
714
+ }
715
+ console.log(chalk.bold(`Credentials (${credentials.length}):\n`));
716
+ for (const cred of credentials) {
717
+ const expired = cred.expiresAt && cred.expiresAt < new Date();
718
+ const status = expired ? chalk.red(' [EXPIRED]') : '';
719
+ console.log(` ${chalk.cyan(cred.name)}${status}`);
720
+ console.log(` Type: ${cred.credentialType}`);
721
+ if (cred.description) {
722
+ console.log(` Description: ${cred.description}`);
723
+ }
724
+ console.log(` Created: ${cred.createdAt.toISOString()}`);
725
+ console.log(` Updated: ${cred.updatedAt.toISOString()}`);
726
+ if (cred.expiresAt) {
727
+ console.log(` Expires: ${cred.expiresAt.toISOString()}`);
728
+ }
729
+ if (cred.tags.length > 0) {
730
+ console.log(` Tags: ${cred.tags.join(', ')}`);
731
+ }
732
+ console.log();
733
+ }
734
+ }
735
+ catch (error) {
736
+ console.log(chalk.red(`Failed to list credentials: ${error}`));
737
+ process.exit(1);
738
+ }
739
+ });
740
+ credentialsCmd
741
+ .command('verify')
742
+ .description('Verify credential encryption is working')
743
+ .option('--state-dir <path>', 'State directory', join('.marktoflow', 'credentials'))
744
+ .option('--backend <backend>', 'Encryption backend (aes-256-gcm, fernet, age, gpg)')
745
+ .action((options) => {
746
+ try {
747
+ const stateDir = options.stateDir;
748
+ const backend = options.backend ?? undefined;
749
+ console.log(chalk.bold('Credential Encryption Verification\n'));
750
+ // Show available backends
751
+ const backends = getAvailableBackends();
752
+ console.log(chalk.bold('Available backends:'));
753
+ for (const b of backends) {
754
+ const isDefault = b === EncryptionBackend.AES_256_GCM;
755
+ const marker = isDefault ? chalk.green(' (default)') : '';
756
+ const selected = (backend ?? EncryptionBackend.AES_256_GCM) === b ? chalk.cyan(' <-- selected') : '';
757
+ console.log(` ${chalk.cyan(b)}${marker}${selected}`);
758
+ }
759
+ console.log();
760
+ // Test encrypt/decrypt round-trip
761
+ const manager = createCredentialManager({ stateDir, backend });
762
+ const testValue = `verify-test-${Date.now()}`;
763
+ const testName = `__verify_test_${Date.now()}`;
764
+ console.log('Testing encrypt/decrypt round-trip...');
765
+ manager.set({ name: testName, value: testValue, tags: ['__test'] });
766
+ const decrypted = manager.get(testName);
767
+ if (decrypted === testValue) {
768
+ console.log(chalk.green(' Round-trip: PASS'));
769
+ }
770
+ else {
771
+ console.log(chalk.red(' Round-trip: FAIL'));
772
+ console.log(chalk.red(` Expected: ${testValue}`));
773
+ console.log(chalk.red(` Got: ${decrypted}`));
774
+ process.exit(1);
775
+ }
776
+ // Verify stored value is encrypted (not plain text)
777
+ const raw = manager.get(testName, false);
778
+ if (raw !== testValue) {
779
+ console.log(chalk.green(' Encryption: PASS (stored value is encrypted)'));
780
+ }
781
+ else {
782
+ console.log(chalk.red(' Encryption: FAIL (stored value is plain text)'));
783
+ process.exit(1);
784
+ }
785
+ // Cleanup test credential
786
+ manager.delete(testName);
787
+ console.log(chalk.green('\n All checks passed.'));
788
+ // Show credential count
789
+ const credentials = manager.list();
790
+ console.log(`\n Stored credentials: ${credentials.length}`);
791
+ }
792
+ catch (error) {
793
+ console.log(chalk.red(`Verification failed: ${error}`));
794
+ process.exit(1);
795
+ }
796
+ });
408
797
  // --- schedule ---
409
798
  const scheduleCmd = program.command('schedule').description('Scheduler management');
410
799
  scheduleCmd
@@ -496,13 +885,14 @@ bundleCmd
496
885
  }
497
886
  const bundle = new WorkflowBundle(path);
498
887
  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
- }
888
+ // Parse and validate inputs
889
+ const parsedInputs = parseInputPairs(options.input);
890
+ const validation = validateAndApplyDefaults(workflow, parsedInputs);
891
+ if (!validation.valid) {
892
+ printMissingInputsError(workflow, validation.missingInputs, 'bundle run', path);
893
+ process.exit(1);
505
894
  }
895
+ const inputs = validation.inputs;
506
896
  const engine = new WorkflowEngine();
507
897
  const registry = new SDKRegistry();
508
898
  registerIntegrations(registry);
@@ -534,6 +924,7 @@ program
534
924
  .option('--client-id <id>', 'OAuth client ID')
535
925
  .option('--client-secret <secret>', 'OAuth client secret')
536
926
  .option('--tenant-id <tenant>', 'Microsoft tenant ID (for Outlook)')
927
+ .option('--port <port>', 'Port for OAuth callback server (default: 8484)', '8484')
537
928
  .action(async (service, options) => {
538
929
  const serviceLower = service.toLowerCase();
539
930
  console.log(chalk.bold(`Connecting ${service}...`));
@@ -541,6 +932,7 @@ program
541
932
  if (serviceLower === 'gmail') {
542
933
  const clientId = options.clientId ?? process.env.GOOGLE_CLIENT_ID;
543
934
  const clientSecret = options.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET;
935
+ const port = parseInt(options.port, 10);
544
936
  if (!clientId || !clientSecret) {
545
937
  console.log(chalk.yellow('\nGmail OAuth requires client credentials.'));
546
938
  console.log('\nTo connect Gmail:');
@@ -554,7 +946,7 @@ program
554
946
  }
555
947
  try {
556
948
  const { runGmailOAuth } = await import('./oauth.js');
557
- const tokens = await runGmailOAuth({ clientId, clientSecret });
949
+ const tokens = await runGmailOAuth({ clientId, clientSecret, port });
558
950
  console.log(chalk.green('\nGmail connected successfully!'));
559
951
  console.log(chalk.dim(`Access token expires: ${tokens.expires_at ? new Date(tokens.expires_at).toISOString() : 'unknown'}`));
560
952
  console.log('\nYou can now use Gmail in your workflows:');
@@ -564,25 +956,77 @@ program
564
956
  auth:
565
957
  client_id: "\${GOOGLE_CLIENT_ID}"
566
958
  client_secret: "\${GOOGLE_CLIENT_SECRET}"
567
- redirect_uri: "http://localhost:8484/callback"
959
+ redirect_uri: "http://localhost:${port}/callback"
568
960
  refresh_token: "\${GMAIL_REFRESH_TOKEN}"`));
961
+ process.exit(0);
962
+ }
963
+ catch (error) {
964
+ console.log(chalk.red(`\nOAuth failed: ${error}`));
965
+ process.exit(1);
966
+ }
967
+ }
968
+ // Handle other Google services (Drive, Sheets, Calendar, Docs, Workspace)
969
+ if (serviceLower === 'google-drive' ||
970
+ serviceLower === 'drive' ||
971
+ serviceLower === 'google-sheets' ||
972
+ serviceLower === 'sheets' ||
973
+ serviceLower === 'google-calendar' ||
974
+ serviceLower === 'calendar' ||
975
+ serviceLower === 'google-docs' ||
976
+ serviceLower === 'docs' ||
977
+ serviceLower === 'google-workspace' ||
978
+ serviceLower === 'workspace') {
979
+ const clientId = options.clientId ?? process.env.GOOGLE_CLIENT_ID;
980
+ const clientSecret = options.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET;
981
+ const port = parseInt(options.port, 10);
982
+ if (!clientId || !clientSecret) {
983
+ console.log(chalk.yellow('\nGoogle OAuth requires client credentials.'));
984
+ console.log('\nTo connect Google services:');
985
+ console.log(' 1. Go to https://console.cloud.google.com/');
986
+ console.log(' 2. Enable the API for your service (Drive, Sheets, etc.)');
987
+ console.log(' 3. Create OAuth 2.0 credentials (Desktop app type)');
988
+ console.log(` 4. Run: marktoflow connect ${service} --client-id YOUR_ID --client-secret YOUR_SECRET`);
989
+ console.log('\nOr set environment variables:');
990
+ console.log(' export GOOGLE_CLIENT_ID="your-client-id"');
991
+ console.log(' export GOOGLE_CLIENT_SECRET="your-client-secret"');
992
+ return;
993
+ }
994
+ try {
995
+ const { runGoogleOAuth } = await import('./oauth.js');
996
+ const tokens = await runGoogleOAuth(serviceLower, { clientId, clientSecret, port });
997
+ console.log(chalk.dim(`Access token expires: ${tokens.expires_at ? new Date(tokens.expires_at).toISOString() : 'unknown'}`));
998
+ // Normalize service name for display
999
+ const normalizedService = serviceLower.startsWith('google-')
1000
+ ? serviceLower
1001
+ : `google-${serviceLower}`;
1002
+ console.log('\nYou can now use this service in your workflows:');
1003
+ console.log(chalk.cyan(` tools:
1004
+ ${serviceLower.replace('google-', '')}:
1005
+ sdk: "${normalizedService}"
1006
+ auth:
1007
+ client_id: "\${GOOGLE_CLIENT_ID}"
1008
+ client_secret: "\${GOOGLE_CLIENT_SECRET}"
1009
+ redirect_uri: "http://localhost:${port}/callback"
1010
+ refresh_token: "\${GOOGLE_REFRESH_TOKEN}"
1011
+ access_token: "\${GOOGLE_ACCESS_TOKEN}"`));
1012
+ process.exit(0);
569
1013
  }
570
1014
  catch (error) {
571
1015
  console.log(chalk.red(`\nOAuth failed: ${error}`));
572
1016
  process.exit(1);
573
1017
  }
574
- return;
575
1018
  }
576
1019
  if (serviceLower === 'outlook' || serviceLower === 'microsoft') {
577
1020
  const clientId = options.clientId ?? process.env.MICROSOFT_CLIENT_ID;
578
1021
  const clientSecret = options.clientSecret ?? process.env.MICROSOFT_CLIENT_SECRET;
579
1022
  const tenantId = options.tenantId ?? process.env.MICROSOFT_TENANT_ID;
1023
+ const port = parseInt(options.port, 10);
580
1024
  if (!clientId) {
581
1025
  console.log(chalk.yellow('\nOutlook OAuth requires a client ID.'));
582
1026
  console.log('\nTo connect Outlook/Microsoft Graph:');
583
1027
  console.log(' 1. Go to https://portal.azure.com/');
584
1028
  console.log(' 2. Register an application in Azure AD');
585
- console.log(' 3. Add redirect URI: http://localhost:8484/callback');
1029
+ console.log(` 3. Add redirect URI: http://localhost:${port}/callback`);
586
1030
  console.log(' 4. Grant Mail.Read, Mail.Send, Calendars.ReadWrite permissions');
587
1031
  console.log(' 5. Run: marktoflow connect outlook --client-id YOUR_ID');
588
1032
  console.log('\nOr set environment variables:');
@@ -593,7 +1037,7 @@ program
593
1037
  }
594
1038
  try {
595
1039
  const { runOutlookOAuth } = await import('./oauth.js');
596
- const tokens = await runOutlookOAuth({ clientId, clientSecret, tenantId });
1040
+ const tokens = await runOutlookOAuth({ clientId, clientSecret, tenantId, port });
597
1041
  console.log(chalk.green('\nOutlook connected successfully!'));
598
1042
  console.log(chalk.dim(`Access token expires: ${tokens.expires_at ? new Date(tokens.expires_at).toISOString() : 'unknown'}`));
599
1043
  console.log('\nYou can now use Outlook in your workflows:');
@@ -602,12 +1046,12 @@ program
602
1046
  sdk: "@microsoft/microsoft-graph-client"
603
1047
  auth:
604
1048
  token: "\${OUTLOOK_ACCESS_TOKEN}"`));
1049
+ process.exit(0);
605
1050
  }
606
1051
  catch (error) {
607
1052
  console.log(chalk.red(`\nOAuth failed: ${error}`));
608
1053
  process.exit(1);
609
1054
  }
610
- return;
611
1055
  }
612
1056
  // Other services - show manual setup instructions
613
1057
  console.log('\nManual setup required. Set environment variables:');
@@ -663,6 +1107,7 @@ program
663
1107
  console.log('\n' + chalk.bold('Available services:'));
664
1108
  console.log(' Communication: slack, discord');
665
1109
  console.log(' Email: gmail, outlook');
1110
+ console.log(' Google Workspace: google-drive, google-sheets, google-calendar, google-docs, google-workspace');
666
1111
  console.log(' Project management: jira, linear');
667
1112
  console.log(' Documentation: notion, confluence');
668
1113
  console.log(' Developer: github');
@@ -736,6 +1181,104 @@ program
736
1181
  console.log(chalk.yellow('\n Run `marktoflow connect <service>` to set up integrations'));
737
1182
  }
738
1183
  });
1184
+ // --- test-connection ---
1185
+ program
1186
+ .command('test-connection [service]')
1187
+ .description('Test service connection(s)')
1188
+ .option('-a, --all', 'Test all configured services')
1189
+ .action(async (service, options) => {
1190
+ await executeTestConnection(service, options);
1191
+ });
1192
+ // --- history ---
1193
+ program
1194
+ .command('history [runId]')
1195
+ .description('View execution history')
1196
+ .option('-n, --limit <count>', 'Number of executions to show', '20')
1197
+ .option('-s, --status <status>', 'Filter by status (completed, failed, running)')
1198
+ .option('-w, --workflow <id>', 'Filter by workflow ID')
1199
+ .option('--step <stepId>', 'Show specific step details (requires runId)')
1200
+ .action((runId, options) => {
1201
+ if (runId) {
1202
+ executeHistoryDetail(runId, { step: options.step });
1203
+ }
1204
+ else {
1205
+ executeHistory({
1206
+ limit: options.limit ? parseInt(options.limit, 10) : 20,
1207
+ status: options.status,
1208
+ workflow: options.workflow,
1209
+ });
1210
+ }
1211
+ });
1212
+ // --- replay ---
1213
+ program
1214
+ .command('replay <runId>')
1215
+ .description('Replay a previous execution with the same inputs')
1216
+ .option('--from <stepId>', 'Resume from specific step')
1217
+ .option('--dry-run', 'Preview what would be executed')
1218
+ .action(async (runId, options) => {
1219
+ await executeReplay(runId, options);
1220
+ });
1221
+ // --- gui ---
1222
+ program
1223
+ .command('gui')
1224
+ .description('Launch visual workflow designer')
1225
+ .option('-p, --port <port>', 'Server port', '3001')
1226
+ .option('-o, --open', 'Open browser automatically')
1227
+ .option('-w, --workflow <path>', 'Open specific workflow')
1228
+ .option('-d, --dir <path>', 'Workflow directory', '.')
1229
+ .action(async (options) => {
1230
+ const spinner = ora('Starting GUI server...').start();
1231
+ try {
1232
+ // Check if @marktoflow/gui is available
1233
+ let guiModule;
1234
+ let guiPackagePath;
1235
+ try {
1236
+ guiModule = await import('@marktoflow/gui');
1237
+ // Find the GUI package location
1238
+ const { createRequire } = await import('node:module');
1239
+ const require = createRequire(import.meta.url);
1240
+ guiPackagePath = require.resolve('@marktoflow/gui');
1241
+ }
1242
+ catch {
1243
+ spinner.fail('@marktoflow/gui package not found');
1244
+ console.log(chalk.yellow('\nTo use the GUI, install the gui package:'));
1245
+ console.log(chalk.cyan(' npm install @marktoflow/gui@alpha'));
1246
+ console.log('\nOr run from the monorepo:');
1247
+ console.log(chalk.cyan(' pnpm --filter @marktoflow/gui dev'));
1248
+ process.exit(1);
1249
+ }
1250
+ spinner.succeed(`GUI server starting on http://localhost:${options.port}`);
1251
+ // Open browser if requested
1252
+ if (options.open) {
1253
+ const url = `http://localhost:${options.port}`;
1254
+ const { exec } = await import('node:child_process');
1255
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1256
+ exec(`${openCmd} ${url}`);
1257
+ }
1258
+ console.log('\n' + chalk.bold('Marktoflow GUI'));
1259
+ console.log(` Server: ${chalk.cyan(`http://localhost:${options.port}`)}`);
1260
+ console.log(` Workflows: ${chalk.cyan(options.dir)}`);
1261
+ console.log('\n Press ' + chalk.bold('Ctrl+C') + ' to stop\n');
1262
+ // Find the static files directory
1263
+ // guiPackagePath is .../node_modules/@marktoflow/gui/dist/server/index.js
1264
+ // We need to go up to the package root: dist/server -> dist -> package root
1265
+ const { dirname, join } = await import('node:path');
1266
+ const guiPackageDir = dirname(dirname(dirname(guiPackagePath)));
1267
+ const staticDir = join(guiPackageDir, 'dist', 'client');
1268
+ // The GUI package will handle the server
1269
+ if (guiModule.startServer) {
1270
+ await guiModule.startServer({
1271
+ port: parseInt(options.port, 10),
1272
+ workflowDir: options.dir,
1273
+ staticDir: staticDir,
1274
+ });
1275
+ }
1276
+ }
1277
+ catch (error) {
1278
+ spinner.fail(`Failed to start GUI: ${error}`);
1279
+ process.exit(1);
1280
+ }
1281
+ });
739
1282
  // --- version ---
740
1283
  program
741
1284
  .command('version')