@marktoflow/cli 2.0.2 → 2.0.4-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +49 -6
  2. package/dist/commands/agent.d.ts +6 -0
  3. package/dist/commands/agent.d.ts.map +1 -0
  4. package/dist/commands/agent.js +56 -0
  5. package/dist/commands/agent.js.map +1 -0
  6. package/dist/commands/bundle.d.ts +11 -0
  7. package/dist/commands/bundle.d.ts.map +1 -0
  8. package/dist/commands/bundle.js +94 -0
  9. package/dist/commands/bundle.js.map +1 -0
  10. package/dist/commands/connect.d.ts +11 -0
  11. package/dist/commands/connect.d.ts.map +1 -0
  12. package/dist/commands/connect.js +203 -0
  13. package/dist/commands/connect.js.map +1 -0
  14. package/dist/commands/credentials.d.ts +16 -0
  15. package/dist/commands/credentials.d.ts.map +1 -0
  16. package/dist/commands/credentials.js +94 -0
  17. package/dist/commands/credentials.js.map +1 -0
  18. package/dist/commands/doctor.d.ts +5 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +60 -0
  21. package/dist/commands/doctor.js.map +1 -0
  22. package/dist/commands/gui.d.ts +11 -0
  23. package/dist/commands/gui.d.ts.map +1 -0
  24. package/dist/commands/gui.js +55 -0
  25. package/dist/commands/gui.js.map +1 -0
  26. package/dist/commands/init.d.ts +8 -0
  27. package/dist/commands/init.d.ts.map +1 -0
  28. package/dist/commands/init.js +79 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/run.d.ts +13 -0
  31. package/dist/commands/run.d.ts.map +1 -0
  32. package/dist/commands/run.js +311 -0
  33. package/dist/commands/run.js.map +1 -0
  34. package/dist/commands/schedule.d.ts +5 -0
  35. package/dist/commands/schedule.d.ts.map +1 -0
  36. package/dist/commands/schedule.js +19 -0
  37. package/dist/commands/schedule.js.map +1 -0
  38. package/dist/commands/template.d.ts +5 -0
  39. package/dist/commands/template.d.ts.map +1 -0
  40. package/dist/commands/template.js +18 -0
  41. package/dist/commands/template.js.map +1 -0
  42. package/dist/commands/tools.d.ts +5 -0
  43. package/dist/commands/tools.d.ts.map +1 -0
  44. package/dist/commands/tools.js +28 -0
  45. package/dist/commands/tools.js.map +1 -0
  46. package/dist/commands/update.js +6 -6
  47. package/dist/commands/update.js.map +1 -1
  48. package/dist/commands/workflow.d.ts +5 -0
  49. package/dist/commands/workflow.d.ts.map +1 -0
  50. package/dist/commands/workflow.js +31 -0
  51. package/dist/commands/workflow.js.map +1 -0
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +52 -1026
  55. package/dist/index.js.map +1 -1
  56. package/dist/serve.js +1 -1
  57. package/dist/serve.js.map +1 -1
  58. package/dist/utils/agent-config.d.ts +12 -0
  59. package/dist/utils/agent-config.d.ts.map +1 -0
  60. package/dist/utils/agent-config.js +52 -0
  61. package/dist/utils/agent-config.js.map +1 -0
  62. package/dist/utils/agent-override.d.ts +1 -1
  63. package/dist/utils/agent-override.d.ts.map +1 -1
  64. package/dist/utils/agent-override.js +3 -1
  65. package/dist/utils/agent-override.js.map +1 -1
  66. package/dist/utils/detect-agents.d.ts +23 -0
  67. package/dist/utils/detect-agents.d.ts.map +1 -0
  68. package/dist/utils/detect-agents.js +125 -0
  69. package/dist/utils/detect-agents.js.map +1 -0
  70. package/dist/utils/index.d.ts +2 -0
  71. package/dist/utils/index.d.ts.map +1 -1
  72. package/dist/utils/index.js +2 -0
  73. package/dist/utils/index.js.map +1 -1
  74. package/package.json +33 -11
package/dist/index.js CHANGED
@@ -3,88 +3,49 @@
3
3
  * marktoflow CLI
4
4
  *
5
5
  * Agent automation framework with native MCP support.
6
+ * This file is the thin routing layer — all command logic lives in ./commands/.
6
7
  */
7
8
  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, createCredentialManager, getAvailableBackends, EncryptionBackend, StateStore, } from '@marktoflow/core';
13
- import { registerIntegrations } from '@marktoflow/integrations';
9
+ import { join, dirname } from 'node:path';
10
+ import { readFileSync } from 'node:fs';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { loadEnv } from '@marktoflow/core';
14
13
  import { workerCommand } from './worker.js';
15
14
  import { triggerCommand } from './trigger.js';
16
15
  import { serveCommand } from './serve.js';
16
+ // Commands
17
+ import { executeInit } from './commands/init.js';
17
18
  import { runWorkflowWizard, listTemplates } from './commands/new.js';
18
19
  import { runUpdateWizard, listAgents } from './commands/update.js';
19
- import { parse as parseYaml } from 'yaml';
20
- import { executeDryRun, displayDryRunSummary } from './commands/dry-run.js';
20
+ import { executeRun } from './commands/run.js';
21
+ import { executeWorkflowList } from './commands/workflow.js';
22
+ import { executeAgentList, executeAgentInfo } from './commands/agent.js';
23
+ import { executeToolsList } from './commands/tools.js';
24
+ import { executeCredentialsList, executeCredentialsVerify } from './commands/credentials.js';
25
+ import { executeScheduleList } from './commands/schedule.js';
26
+ import { executeBundleList, executeBundleInfo, executeBundleValidate, executeBundleRun } from './commands/bundle.js';
27
+ import { executeTemplateList } from './commands/template.js';
28
+ import { executeConnect } from './commands/connect.js';
29
+ import { executeDoctor } from './commands/doctor.js';
30
+ import { executeGui } from './commands/gui.js';
21
31
  import { WorkflowDebugger, parseBreakpoints } from './commands/debug.js';
22
32
  import { executeTestConnection } from './commands/test-connection.js';
23
33
  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.2';
34
+ // Utils used by debug command
35
+ import { parseInputPairs, validateAndApplyDefaults, printMissingInputsError, overrideAgentInWorkflow, overrideModelInWorkflow, getAgentSDKName, getAgentAuthConfig, } from './utils/index.js';
36
+ import { parseFile, SDKRegistry, createSDKStepExecutor, loadConfig, } from '@marktoflow/core';
37
+ import { registerIntegrations } from '@marktoflow/integrations';
38
+ import { existsSync } from 'node:fs';
39
+ import chalk from 'chalk';
40
+ import ora from 'ora';
41
+ // Read version from package.json
42
+ const __filename = fileURLToPath(import.meta.url);
43
+ const __dirname = dirname(__filename);
44
+ const packageJsonPath = join(__dirname, '../package.json');
45
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
46
+ const VERSION = packageJson.version;
26
47
  // Load environment variables from .env files on CLI startup
27
48
  loadEnv();
28
- function getConfig() {
29
- return loadConfig(process.cwd());
30
- }
31
- function isBundle(path) {
32
- try {
33
- const stat = existsSync(path) ? statSync(path) : null;
34
- if (!stat || !stat.isDirectory())
35
- return false;
36
- const entries = readdirSync(path);
37
- return entries.some((name) => name.endsWith('.md') && name !== 'README.md');
38
- }
39
- catch {
40
- return false;
41
- }
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
- }
88
49
  // ============================================================================
89
50
  // CLI Setup
90
51
  // ============================================================================
@@ -104,63 +65,7 @@ program
104
65
  .command('init')
105
66
  .description('Initialize a new marktoflow project')
106
67
  .option('-f, --force', 'Overwrite existing configuration')
107
- .action(async (options) => {
108
- const spinner = ora('Initializing marktoflow project...').start();
109
- try {
110
- const configDir = '.marktoflow';
111
- const workflowsDir = join(configDir, 'workflows');
112
- const credentialsDir = join(configDir, 'credentials');
113
- if (existsSync(configDir) && !options.force) {
114
- spinner.fail('Project already initialized. Use --force to reinitialize.');
115
- return;
116
- }
117
- // Create directories
118
- mkdirSync(workflowsDir, { recursive: true });
119
- mkdirSync(credentialsDir, { recursive: true });
120
- // Create example workflow
121
- const exampleWorkflow = `---
122
- workflow:
123
- id: hello-world
124
- name: "Hello World"
125
- version: "1.0.0"
126
- description: "A simple example workflow"
127
-
128
- # Uncomment and configure to use Slack:
129
- # tools:
130
- # slack:
131
- # sdk: "@slack/web-api"
132
- # auth:
133
- # token: "\${SLACK_BOT_TOKEN}"
134
-
135
- steps:
136
- - id: greet
137
- action: core.log
138
- inputs:
139
- message: "Hello from marktoflow!"
140
- ---
141
-
142
- # Hello World Workflow
143
-
144
- This is a simple example workflow.
145
-
146
- ## Step 1: Greet
147
-
148
- Outputs a greeting message.
149
- `;
150
- writeFileSync(join(workflowsDir, 'hello-world.md'), exampleWorkflow);
151
- // Create .gitignore for credentials
152
- writeFileSync(join(credentialsDir, '.gitignore'), '# Ignore all credentials\n*\n!.gitignore\n');
153
- spinner.succeed('Project initialized successfully!');
154
- console.log('\n' + chalk.bold('Next steps:'));
155
- console.log(` 1. Edit ${chalk.cyan('.marktoflow/workflows/hello-world.md')}`);
156
- console.log(` 2. Run ${chalk.cyan('marktoflow run hello-world.md')}`);
157
- console.log(` 3. Connect services: ${chalk.cyan('marktoflow connect slack')}`);
158
- }
159
- catch (error) {
160
- spinner.fail(`Initialization failed: ${error}`);
161
- process.exit(1);
162
- }
163
- });
68
+ .action(async (options) => executeInit(options));
164
69
  // --- new ---
165
70
  program
166
71
  .command('new')
@@ -179,7 +84,7 @@ program
179
84
  program
180
85
  .command('update [workflow]')
181
86
  .description('Update a workflow using AI coding agents')
182
- .option('-a, --agent <id>', 'Coding agent to use (opencode, claude-code, cursor, aider)')
87
+ .option('-a, --agent <id>', 'Coding agent to use (opencode, claude-agent, openai, ollama)')
183
88
  .option('-p, --prompt <text>', 'Update description')
184
89
  .option('--list-agents', 'List available coding agents')
185
90
  .action(async (workflow, options) => {
@@ -205,332 +110,24 @@ program
205
110
  .option('-i, --input <key=value...>', 'Input parameters')
206
111
  .option('-v, --verbose', 'Verbose output')
207
112
  .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)')
113
+ .option('-a, --agent <provider>', 'AI agent provider (claude-agent, openai, github-copilot, opencode, ollama, codex)')
209
114
  .option('-m, --model <name>', 'Model name to use (e.g., claude-sonnet-4, gpt-4, etc.)')
210
115
  .option('--dry-run', 'Parse workflow without executing')
211
- .action(async (workflowPath, options) => {
212
- const spinner = ora('Loading workflow...').start();
213
- let stateStore;
214
- try {
215
- const config = getConfig();
216
- const workflowsDir = config.workflows?.path ?? '.marktoflow/workflows';
217
- // Resolve workflow path
218
- let resolvedPath = workflowPath;
219
- if (!existsSync(resolvedPath)) {
220
- resolvedPath = join(workflowsDir, workflowPath);
221
- }
222
- if (!existsSync(resolvedPath)) {
223
- spinner.fail(`Workflow not found: ${workflowPath}`);
224
- process.exit(1);
225
- }
226
- // Parse workflow
227
- const { workflow, warnings } = await parseFile(resolvedPath);
228
- if (warnings.length > 0) {
229
- spinner.warn('Workflow parsed with warnings:');
230
- warnings.forEach((w) => console.log(chalk.yellow(` - ${w}`)));
231
- }
232
- else {
233
- spinner.succeed(`Loaded: ${workflow.metadata.name}`);
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
- }
244
- // Parse inputs
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`));
285
- }
286
- }
287
- // Handle dry-run mode
288
- if (options.dryRun) {
289
- const dryRunResult = await executeDryRun(workflow, inputs, {
290
- verbose: options.verbose,
291
- showMockData: true,
292
- showVariables: true,
293
- });
294
- displayDryRunSummary(dryRunResult, {
295
- verbose: options.verbose,
296
- showMockData: true,
297
- showVariables: true,
298
- });
299
- return;
300
- }
301
- // Execute workflow
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'));
315
- const engine = new WorkflowEngine({}, {
316
- onStepStart: (step) => {
317
- if (options.verbose || options.debug) {
318
- spinner.text = `Executing: ${step.id}`;
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
- }
329
- },
330
- onStepComplete: (step, result) => {
331
- if (options.verbose || options.debug) {
332
- const icon = result.status === StepStatus.COMPLETED ? '✓' : '✗';
333
- console.log(` ${icon} ${step.id}: ${result.status}`);
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
- }
370
- },
371
- }, stateStore);
372
- // Set workflow path for execution history
373
- engine.workflowPath = resolvedPath;
374
- const registry = new SDKRegistry();
375
- registerIntegrations(registry);
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
- }
382
- const result = await engine.execute(workflow, inputs, registry, createSDKStepExecutor());
383
- if (result.status === WorkflowStatus.COMPLETED) {
384
- spinner.succeed(`Workflow completed in ${result.duration}ms`);
385
- }
386
- else {
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
- }
469
- process.exit(1);
470
- }
471
- // Show summary
472
- console.log('\n' + chalk.bold('Summary:'));
473
- console.log(` Status: ${result.status}`);
474
- console.log(` Duration: ${result.duration}ms`);
475
- console.log(` Steps: ${result.stepResults.length}`);
476
- const completed = result.stepResults.filter((s) => s.status === StepStatus.COMPLETED).length;
477
- const failed = result.stepResults.filter((s) => s.status === StepStatus.FAILED).length;
478
- const skipped = result.stepResults.filter((s) => s.status === StepStatus.SKIPPED).length;
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);
483
- }
484
- catch (error) {
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();
516
- process.exit(1);
517
- }
518
- });
116
+ .action(async (workflowPath, options) => executeRun(workflowPath, options));
519
117
  // --- debug ---
520
118
  program
521
119
  .command('debug <workflow>')
522
120
  .description('Debug a workflow with step-by-step execution')
523
121
  .option('-i, --input <key=value...>', 'Input parameters')
524
122
  .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)')
123
+ .option('-a, --agent <provider>', 'AI agent provider (claude-agent, openai, github-copilot, opencode, ollama, codex)')
526
124
  .option('-m, --model <name>', 'Model name to use (e.g., claude-sonnet-4, gpt-4, etc.)')
527
125
  .option('--auto-start', 'Start without initial prompt')
528
126
  .action(async (workflowPath, options) => {
529
127
  const spinner = ora('Loading workflow for debugging...').start();
530
128
  try {
531
- const config = getConfig();
129
+ const config = loadConfig(process.cwd());
532
130
  const workflowsDir = config.workflows?.path ?? '.marktoflow/workflows';
533
- // Resolve workflow path
534
131
  let resolvedPath = workflowPath;
535
132
  if (!existsSync(resolvedPath)) {
536
133
  resolvedPath = join(workflowsDir, workflowPath);
@@ -539,7 +136,6 @@ program
539
136
  spinner.fail(`Workflow not found: ${workflowPath}`);
540
137
  process.exit(1);
541
138
  }
542
- // Parse workflow
543
139
  const { workflow, warnings } = await parseFile(resolvedPath);
544
140
  if (warnings.length > 0) {
545
141
  spinner.warn('Workflow parsed with warnings:');
@@ -548,7 +144,6 @@ program
548
144
  else {
549
145
  spinner.succeed(`Loaded: ${workflow.metadata.name}`);
550
146
  }
551
- // Parse and validate inputs
552
147
  const parsedInputs = parseInputPairs(options.input);
553
148
  const validation = validateAndApplyDefaults(workflow, parsedInputs);
554
149
  if (!validation.valid) {
@@ -557,28 +152,22 @@ program
557
152
  process.exit(1);
558
153
  }
559
154
  const inputs = validation.inputs;
560
- // Override AI agent if specified
561
155
  if (options.agent) {
562
156
  const sdkName = getAgentSDKName(options.agent);
563
157
  const authConfig = getAgentAuthConfig(sdkName);
564
158
  overrideAgentInWorkflow(workflow, sdkName, authConfig);
565
159
  }
566
- // Override model if specified
567
160
  if (options.model) {
568
161
  overrideModelInWorkflow(workflow, options.model);
569
162
  }
570
- // Parse breakpoints
571
163
  const breakpoints = options.breakpoint ? parseBreakpoints(options.breakpoint) : [];
572
- // Setup SDK registry and executor
573
164
  const registry = new SDKRegistry();
574
165
  registerIntegrations(registry);
575
166
  registry.registerTools(workflow.tools);
576
- // Create debugger
577
167
  const workflowDebugger = new WorkflowDebugger(workflow, inputs, registry, createSDKStepExecutor(), {
578
168
  breakpoints,
579
169
  autoStart: options.autoStart,
580
170
  });
581
- // Start debugging session
582
171
  await workflowDebugger.debug();
583
172
  }
584
173
  catch (error) {
@@ -592,107 +181,23 @@ program
592
181
  .description('Workflow management')
593
182
  .command('list')
594
183
  .description('List available workflows')
595
- .action(async () => {
596
- const workflowsDir = getConfig().workflows?.path ?? '.marktoflow/workflows';
597
- if (!existsSync(workflowsDir)) {
598
- console.log(chalk.yellow('No workflows found. Run `marktoflow init` first.'));
599
- return;
600
- }
601
- const { readdirSync } = await import('node:fs');
602
- const files = readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
603
- if (files.length === 0) {
604
- console.log(chalk.yellow('No workflows found.'));
605
- return;
606
- }
607
- console.log(chalk.bold('Available Workflows:'));
608
- for (const file of files) {
609
- try {
610
- const { workflow } = await parseFile(join(workflowsDir, file));
611
- console.log(` ${chalk.cyan(file)}: ${workflow.metadata.name}`);
612
- }
613
- catch {
614
- console.log(` ${chalk.red(file)}: (invalid)`);
615
- }
616
- }
617
- });
184
+ .action(async () => executeWorkflowList());
618
185
  // --- agent ---
619
186
  const agentCmd = program.command('agent').description('Agent management');
620
187
  agentCmd
621
188
  .command('list')
622
189
  .description('List available agents')
623
- .action(() => {
624
- const capabilitiesPath = join('.marktoflow', 'agents', 'capabilities.yaml');
625
- const agentsFromFile = [];
626
- if (existsSync(capabilitiesPath)) {
627
- const content = readFileSync(capabilitiesPath, 'utf8');
628
- const data = parseYaml(content);
629
- agentsFromFile.push(...Object.keys(data?.agents ?? {}));
630
- }
631
- const knownAgents = ['claude-code', 'opencode', 'ollama', 'codex', 'gemini-cli'];
632
- const allAgents = Array.from(new Set([...agentsFromFile, ...knownAgents]));
633
- console.log(chalk.bold('Available Agents:'));
634
- for (const agent of allAgents) {
635
- const status = agentsFromFile.includes(agent)
636
- ? chalk.green('Registered')
637
- : chalk.yellow('Not configured');
638
- console.log(` ${chalk.cyan(agent)}: ${status}`);
639
- }
640
- });
190
+ .action(() => executeAgentList());
641
191
  agentCmd
642
192
  .command('info <agent>')
643
193
  .description('Show agent information')
644
- .action((agent) => {
645
- const capabilitiesPath = join('.marktoflow', 'agents', 'capabilities.yaml');
646
- if (!existsSync(capabilitiesPath)) {
647
- console.log(chalk.yellow('No capabilities file found. Run `marktoflow init` first.'));
648
- process.exit(1);
649
- }
650
- const content = readFileSync(capabilitiesPath, 'utf8');
651
- const data = parseYaml(content);
652
- const info = data?.agents?.[agent];
653
- if (!info) {
654
- console.log(chalk.red(`Agent not found: ${agent}`));
655
- process.exit(1);
656
- }
657
- console.log(chalk.bold(agent));
658
- console.log(` Version: ${info.version ?? 'unknown'}`);
659
- console.log(` Provider: ${info.provider ?? 'unknown'}`);
660
- const capabilities = info.capabilities ?? {};
661
- for (const [key, value] of Object.entries(capabilities)) {
662
- if (typeof value === 'object' && value) {
663
- for (const [subKey, subValue] of Object.entries(value)) {
664
- console.log(` ${key}.${subKey}: ${String(subValue)}`);
665
- }
666
- }
667
- else {
668
- console.log(` ${key}: ${String(value)}`);
669
- }
670
- }
671
- });
194
+ .action((agentId) => executeAgentInfo(agentId));
672
195
  // --- tools ---
673
196
  const toolsCmd = program.command('tools').description('Tool management');
674
197
  toolsCmd
675
198
  .command('list')
676
199
  .description('List available tools')
677
- .action(() => {
678
- const registryPath = getConfig().tools?.registryPath ?? join('.marktoflow', 'tools', 'registry.yaml');
679
- if (!existsSync(registryPath)) {
680
- console.log(chalk.yellow("No tool registry found. Run 'marktoflow init' first."));
681
- return;
682
- }
683
- const registry = new ToolRegistry(registryPath);
684
- const tools = registry.listTools();
685
- if (tools.length === 0) {
686
- console.log(chalk.yellow('No tools registered.'));
687
- return;
688
- }
689
- console.log(chalk.bold('Registered Tools:'));
690
- for (const toolName of tools) {
691
- const definition = registry.getDefinition(toolName);
692
- const types = definition?.implementations.map((impl) => impl.type).join(', ') ?? '';
693
- console.log(` ${chalk.cyan(toolName)} ${types ? `(${types})` : ''}`);
694
- }
695
- });
200
+ .action(() => executeToolsList());
696
201
  // --- credentials ---
697
202
  const credentialsCmd = program.command('credentials').description('Credential management');
698
203
  credentialsCmd
@@ -702,221 +207,44 @@ credentialsCmd
702
207
  .option('--backend <backend>', 'Encryption backend (aes-256-gcm, fernet, age, gpg)')
703
208
  .option('--tag <tag>', 'Filter by tag')
704
209
  .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
- });
210
+ .action((options) => executeCredentialsList(options));
740
211
  credentialsCmd
741
212
  .command('verify')
742
213
  .description('Verify credential encryption is working')
743
214
  .option('--state-dir <path>', 'State directory', join('.marktoflow', 'credentials'))
744
215
  .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
- });
216
+ .action((options) => executeCredentialsVerify(options));
797
217
  // --- schedule ---
798
218
  const scheduleCmd = program.command('schedule').description('Scheduler management');
799
219
  scheduleCmd
800
220
  .command('list')
801
221
  .description('List scheduled workflows')
802
- .action(() => {
803
- const scheduler = new Scheduler();
804
- const jobs = scheduler.listJobs();
805
- if (jobs.length === 0) {
806
- console.log(chalk.yellow('No scheduled workflows found.'));
807
- console.log('Add schedule triggers to your workflows to enable scheduling.');
808
- return;
809
- }
810
- console.log(chalk.bold('Scheduled Workflows:'));
811
- for (const job of jobs) {
812
- console.log(` ${chalk.cyan(job.id)} ${job.workflowPath} (${job.schedule})`);
813
- }
814
- });
222
+ .action(() => executeScheduleList());
815
223
  // --- bundle ---
816
224
  const bundleCmd = program.command('bundle').description('Workflow bundle commands');
817
225
  bundleCmd
818
226
  .command('list [path]')
819
227
  .description('List workflow bundles in a directory')
820
- .action((path = '.') => {
821
- if (!existsSync(path)) {
822
- console.log(chalk.red(`Path not found: ${path}`));
823
- process.exit(1);
824
- }
825
- const entries = readdirSync(path, { withFileTypes: true });
826
- const bundles = [];
827
- for (const entry of entries) {
828
- if (!entry.isDirectory())
829
- continue;
830
- const fullPath = join(path, entry.name);
831
- if (isBundle(fullPath))
832
- bundles.push(fullPath);
833
- }
834
- if (bundles.length === 0) {
835
- console.log(chalk.yellow(`No bundles found in ${path}`));
836
- return;
837
- }
838
- console.log(chalk.bold('Bundles:'));
839
- for (const bundlePath of bundles) {
840
- console.log(` ${chalk.cyan(bundlePath)}`);
841
- }
842
- });
228
+ .action((path) => executeBundleList(path));
843
229
  bundleCmd
844
230
  .command('info <path>')
845
231
  .description('Show information about a workflow bundle')
846
- .action(async (path) => {
847
- if (!isBundle(path)) {
848
- console.log(chalk.red(`Not a valid bundle: ${path}`));
849
- process.exit(1);
850
- }
851
- const bundle = new WorkflowBundle(path);
852
- const workflow = await bundle.loadWorkflow();
853
- const tools = bundle.loadTools().listTools();
854
- console.log(chalk.bold(`Bundle: ${bundle.name}`));
855
- console.log(` Workflow: ${workflow.metadata.name} (${workflow.metadata.id})`);
856
- console.log(` Steps: ${workflow.steps.length}`);
857
- console.log(` Tools: ${tools.length ? tools.join(', ') : 'none'}`);
858
- });
232
+ .action(async (path) => executeBundleInfo(path));
859
233
  bundleCmd
860
234
  .command('validate <path>')
861
235
  .description('Validate a workflow bundle')
862
- .action(async (path) => {
863
- if (!isBundle(path)) {
864
- console.log(chalk.red(`Not a valid bundle: ${path}`));
865
- process.exit(1);
866
- }
867
- try {
868
- const bundle = new WorkflowBundle(path);
869
- await bundle.loadWorkflow();
870
- console.log(chalk.green(`Bundle '${bundle.name}' is valid.`));
871
- }
872
- catch (error) {
873
- console.log(chalk.red(`Bundle validation failed: ${error}`));
874
- process.exit(1);
875
- }
876
- });
236
+ .action(async (path) => executeBundleValidate(path));
877
237
  bundleCmd
878
238
  .command('run <path>')
879
239
  .description('Run a workflow bundle')
880
240
  .option('-i, --input <key=value...>', 'Input parameters')
881
- .action(async (path, options) => {
882
- if (!isBundle(path)) {
883
- console.log(chalk.red(`Not a valid bundle: ${path}`));
884
- process.exit(1);
885
- }
886
- const bundle = new WorkflowBundle(path);
887
- const workflow = await bundle.loadWorkflowWithBundleTools();
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);
894
- }
895
- const inputs = validation.inputs;
896
- const engine = new WorkflowEngine();
897
- const registry = new SDKRegistry();
898
- registerIntegrations(registry);
899
- registry.registerTools(workflow.tools);
900
- const result = await engine.execute(workflow, inputs, registry, createSDKStepExecutor());
901
- console.log(chalk.bold(`Bundle completed: ${result.status}`));
902
- });
241
+ .action(async (path, options) => executeBundleRun(path, options));
903
242
  // --- template ---
904
243
  const templateCmd = program.command('template').description('Workflow template commands');
905
244
  templateCmd
906
245
  .command('list')
907
246
  .description('List workflow templates')
908
- .action(() => {
909
- const registry = new TemplateRegistry();
910
- const templates = registry.list();
911
- if (!templates.length) {
912
- console.log(chalk.yellow('No templates found.'));
913
- return;
914
- }
915
- console.log(chalk.bold('Templates:'));
916
- for (const template of templates) {
917
- console.log(` ${chalk.cyan(template.id)}: ${template.name}`);
918
- }
919
- });
247
+ .action(() => executeTemplateList());
920
248
  // --- connect ---
921
249
  program
922
250
  .command('connect <service>')
@@ -925,262 +253,12 @@ program
925
253
  .option('--client-secret <secret>', 'OAuth client secret')
926
254
  .option('--tenant-id <tenant>', 'Microsoft tenant ID (for Outlook)')
927
255
  .option('--port <port>', 'Port for OAuth callback server (default: 8484)', '8484')
928
- .action(async (service, options) => {
929
- const serviceLower = service.toLowerCase();
930
- console.log(chalk.bold(`Connecting ${service}...`));
931
- // Services that support OAuth flow
932
- if (serviceLower === 'gmail') {
933
- const clientId = options.clientId ?? process.env.GOOGLE_CLIENT_ID;
934
- const clientSecret = options.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET;
935
- const port = parseInt(options.port, 10);
936
- if (!clientId || !clientSecret) {
937
- console.log(chalk.yellow('\nGmail OAuth requires client credentials.'));
938
- console.log('\nTo connect Gmail:');
939
- console.log(' 1. Go to https://console.cloud.google.com/');
940
- console.log(' 2. Create OAuth 2.0 credentials (Desktop app type)');
941
- console.log(' 3. Run: marktoflow connect gmail --client-id YOUR_ID --client-secret YOUR_SECRET');
942
- console.log('\nOr set environment variables:');
943
- console.log(' export GOOGLE_CLIENT_ID="your-client-id"');
944
- console.log(' export GOOGLE_CLIENT_SECRET="your-client-secret"');
945
- return;
946
- }
947
- try {
948
- const { runGmailOAuth } = await import('./oauth.js');
949
- const tokens = await runGmailOAuth({ clientId, clientSecret, port });
950
- console.log(chalk.green('\nGmail connected successfully!'));
951
- console.log(chalk.dim(`Access token expires: ${tokens.expires_at ? new Date(tokens.expires_at).toISOString() : 'unknown'}`));
952
- console.log('\nYou can now use Gmail in your workflows:');
953
- console.log(chalk.cyan(` tools:
954
- gmail:
955
- sdk: "googleapis"
956
- auth:
957
- client_id: "\${GOOGLE_CLIENT_ID}"
958
- client_secret: "\${GOOGLE_CLIENT_SECRET}"
959
- redirect_uri: "http://localhost:${port}/callback"
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);
1013
- }
1014
- catch (error) {
1015
- console.log(chalk.red(`\nOAuth failed: ${error}`));
1016
- process.exit(1);
1017
- }
1018
- }
1019
- if (serviceLower === 'outlook' || serviceLower === 'microsoft') {
1020
- const clientId = options.clientId ?? process.env.MICROSOFT_CLIENT_ID;
1021
- const clientSecret = options.clientSecret ?? process.env.MICROSOFT_CLIENT_SECRET;
1022
- const tenantId = options.tenantId ?? process.env.MICROSOFT_TENANT_ID;
1023
- const port = parseInt(options.port, 10);
1024
- if (!clientId) {
1025
- console.log(chalk.yellow('\nOutlook OAuth requires a client ID.'));
1026
- console.log('\nTo connect Outlook/Microsoft Graph:');
1027
- console.log(' 1. Go to https://portal.azure.com/');
1028
- console.log(' 2. Register an application in Azure AD');
1029
- console.log(` 3. Add redirect URI: http://localhost:${port}/callback`);
1030
- console.log(' 4. Grant Mail.Read, Mail.Send, Calendars.ReadWrite permissions');
1031
- console.log(' 5. Run: marktoflow connect outlook --client-id YOUR_ID');
1032
- console.log('\nOr set environment variables:');
1033
- console.log(' export MICROSOFT_CLIENT_ID="your-client-id"');
1034
- console.log(' export MICROSOFT_CLIENT_SECRET="your-client-secret" # optional');
1035
- console.log(' export MICROSOFT_TENANT_ID="common" # or your tenant ID');
1036
- return;
1037
- }
1038
- try {
1039
- const { runOutlookOAuth } = await import('./oauth.js');
1040
- const tokens = await runOutlookOAuth({ clientId, clientSecret, tenantId, port });
1041
- console.log(chalk.green('\nOutlook connected successfully!'));
1042
- console.log(chalk.dim(`Access token expires: ${tokens.expires_at ? new Date(tokens.expires_at).toISOString() : 'unknown'}`));
1043
- console.log('\nYou can now use Outlook in your workflows:');
1044
- console.log(chalk.cyan(` tools:
1045
- outlook:
1046
- sdk: "@microsoft/microsoft-graph-client"
1047
- auth:
1048
- token: "\${OUTLOOK_ACCESS_TOKEN}"`));
1049
- process.exit(0);
1050
- }
1051
- catch (error) {
1052
- console.log(chalk.red(`\nOAuth failed: ${error}`));
1053
- process.exit(1);
1054
- }
1055
- }
1056
- // Other services - show manual setup instructions
1057
- console.log('\nManual setup required. Set environment variables:');
1058
- switch (serviceLower) {
1059
- case 'slack':
1060
- console.log(` export SLACK_BOT_TOKEN="xoxb-your-token"`);
1061
- console.log(` export SLACK_APP_TOKEN="xapp-your-token"`);
1062
- console.log(chalk.dim('\n Get tokens from https://api.slack.com/apps'));
1063
- break;
1064
- case 'github':
1065
- console.log(` export GITHUB_TOKEN="ghp_your-token"`);
1066
- console.log(chalk.dim('\n Create token at https://github.com/settings/tokens'));
1067
- break;
1068
- case 'jira':
1069
- console.log(` export JIRA_HOST="https://your-domain.atlassian.net"`);
1070
- console.log(` export JIRA_EMAIL="your-email@example.com"`);
1071
- console.log(` export JIRA_API_TOKEN="your-api-token"`);
1072
- console.log(chalk.dim('\n Create token at https://id.atlassian.com/manage-profile/security/api-tokens'));
1073
- break;
1074
- case 'confluence':
1075
- console.log(` export CONFLUENCE_HOST="https://your-domain.atlassian.net"`);
1076
- console.log(` export CONFLUENCE_EMAIL="your-email@example.com"`);
1077
- console.log(` export CONFLUENCE_API_TOKEN="your-api-token"`);
1078
- console.log(chalk.dim('\n Create token at https://id.atlassian.com/manage-profile/security/api-tokens'));
1079
- break;
1080
- case 'linear':
1081
- console.log(` export LINEAR_API_KEY="lin_api_your-key"`);
1082
- console.log(chalk.dim('\n Create key at https://linear.app/settings/api'));
1083
- break;
1084
- case 'notion':
1085
- console.log(` export NOTION_TOKEN="secret_your-token"`);
1086
- console.log(chalk.dim('\n Create integration at https://www.notion.so/my-integrations'));
1087
- break;
1088
- case 'discord':
1089
- console.log(` export DISCORD_BOT_TOKEN="your-bot-token"`);
1090
- console.log(chalk.dim('\n Create bot at https://discord.com/developers/applications'));
1091
- break;
1092
- case 'airtable':
1093
- console.log(` export AIRTABLE_TOKEN="pat_your-token"`);
1094
- console.log(` export AIRTABLE_BASE_ID="appXXXXX" # optional default base`);
1095
- console.log(chalk.dim('\n Create token at https://airtable.com/create/tokens'));
1096
- break;
1097
- case 'anthropic':
1098
- console.log(` export ANTHROPIC_API_KEY="sk-ant-your-key"`);
1099
- console.log(chalk.dim('\n Get key at https://console.anthropic.com/'));
1100
- break;
1101
- case 'openai':
1102
- console.log(` export OPENAI_API_KEY="sk-your-key"`);
1103
- console.log(chalk.dim('\n Get key at https://platform.openai.com/api-keys'));
1104
- break;
1105
- default:
1106
- console.log(` See documentation for ${service} configuration.`);
1107
- console.log('\n' + chalk.bold('Available services:'));
1108
- console.log(' Communication: slack, discord');
1109
- console.log(' Email: gmail, outlook');
1110
- console.log(' Google Workspace: google-drive, google-sheets, google-calendar, google-docs, google-workspace');
1111
- console.log(' Project management: jira, linear');
1112
- console.log(' Documentation: notion, confluence');
1113
- console.log(' Developer: github');
1114
- console.log(' Data: airtable');
1115
- console.log(' AI: anthropic, openai');
1116
- }
1117
- });
256
+ .action(async (service, options) => executeConnect(service, options));
1118
257
  // --- doctor ---
1119
258
  program
1120
259
  .command('doctor')
1121
260
  .description('Check environment and configuration')
1122
- .action(async () => {
1123
- console.log(chalk.bold('marktoflow Doctor\n'));
1124
- // Node version
1125
- const nodeVersion = process.version;
1126
- const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
1127
- if (nodeMajor >= 20) {
1128
- console.log(chalk.green('✓') + ` Node.js ${nodeVersion}`);
1129
- }
1130
- else {
1131
- console.log(chalk.red('✗') + ` Node.js ${nodeVersion} (requires >=20)`);
1132
- }
1133
- // Project initialized
1134
- if (existsSync('.marktoflow')) {
1135
- console.log(chalk.green('✓') + ' Project initialized');
1136
- // Count workflows
1137
- const workflowsDir = '.marktoflow/workflows';
1138
- if (existsSync(workflowsDir)) {
1139
- const { readdirSync } = await import('node:fs');
1140
- const workflows = readdirSync(workflowsDir).filter((f) => f.endsWith('.md'));
1141
- console.log(chalk.green('✓') + ` ${workflows.length} workflow(s) found`);
1142
- }
1143
- }
1144
- else {
1145
- console.log(chalk.yellow('○') + ' Project not initialized');
1146
- }
1147
- // Check for common environment variables
1148
- const envChecks = [
1149
- // Communication
1150
- ['SLACK_BOT_TOKEN', 'Slack'],
1151
- ['DISCORD_BOT_TOKEN', 'Discord'],
1152
- // Email
1153
- ['GOOGLE_CLIENT_ID', 'Gmail'],
1154
- ['MICROSOFT_CLIENT_ID', 'Outlook'],
1155
- // Project Management
1156
- ['JIRA_API_TOKEN', 'Jira'],
1157
- ['LINEAR_API_KEY', 'Linear'],
1158
- // Documentation
1159
- ['NOTION_TOKEN', 'Notion'],
1160
- ['CONFLUENCE_API_TOKEN', 'Confluence'],
1161
- // Developer
1162
- ['GITHUB_TOKEN', 'GitHub'],
1163
- // Data
1164
- ['AIRTABLE_TOKEN', 'Airtable'],
1165
- // AI
1166
- ['ANTHROPIC_API_KEY', 'Anthropic'],
1167
- ['OPENAI_API_KEY', 'OpenAI'],
1168
- ];
1169
- console.log('\n' + chalk.bold('Services:'));
1170
- let configuredCount = 0;
1171
- for (const [envVar, name] of envChecks) {
1172
- if (process.env[envVar]) {
1173
- console.log(chalk.green('✓') + ` ${name} configured`);
1174
- configuredCount++;
1175
- }
1176
- else {
1177
- console.log(chalk.dim('○') + ` ${name} not configured`);
1178
- }
1179
- }
1180
- if (configuredCount === 0) {
1181
- console.log(chalk.yellow('\n Run `marktoflow connect <service>` to set up integrations'));
1182
- }
1183
- });
261
+ .action(async () => executeDoctor());
1184
262
  // --- test-connection ---
1185
263
  program
1186
264
  .command('test-connection [service]')
@@ -1226,59 +304,7 @@ program
1226
304
  .option('-o, --open', 'Open browser automatically')
1227
305
  .option('-w, --workflow <path>', 'Open specific workflow')
1228
306
  .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
- });
307
+ .action(async (options) => executeGui(options));
1282
308
  // --- version ---
1283
309
  program
1284
310
  .command('version')