@artemiskit/cli 0.2.3 → 0.3.0

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.
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Validate command - Validate scenarios without running them
3
+ */
4
+
5
+ import { readdirSync, statSync } from 'node:fs';
6
+ import { mkdir, writeFile } from 'node:fs/promises';
7
+ import { basename, join, resolve } from 'node:path';
8
+ import { ScenarioValidator, type ValidationResult, type ValidationSummary } from '@artemiskit/core';
9
+ import { generateValidationJUnitReport } from '@artemiskit/reports';
10
+ import { Glob } from 'bun';
11
+ import chalk from 'chalk';
12
+ import { Command } from 'commander';
13
+ import { icons } from '../ui/index.js';
14
+
15
+ interface ValidateOptions {
16
+ json?: boolean;
17
+ strict?: boolean;
18
+ quiet?: boolean;
19
+ export?: 'junit';
20
+ exportOutput?: string;
21
+ }
22
+
23
+ export function validateCommand(): Command {
24
+ const cmd = new Command('validate');
25
+
26
+ cmd
27
+ .description('Validate scenario files without running them')
28
+ .argument('<path>', 'Path to scenario file, directory, or glob pattern')
29
+ .option('--json', 'Output as JSON')
30
+ .option('--strict', 'Treat warnings as errors')
31
+ .option('-q, --quiet', 'Only output errors (no success messages)')
32
+ .option('--export <format>', 'Export results to format (junit for CI integration)')
33
+ .option('--export-output <dir>', 'Output directory for exports (default: ./artemis-exports)')
34
+ .action(async (pathArg: string, options: ValidateOptions) => {
35
+ const validator = new ScenarioValidator({ strict: options.strict });
36
+
37
+ // Resolve files to validate
38
+ const files = resolveFiles(pathArg);
39
+
40
+ if (files.length === 0) {
41
+ if (options.json) {
42
+ console.log(
43
+ JSON.stringify(
44
+ {
45
+ valid: false,
46
+ error: `No scenario files found matching: ${pathArg}`,
47
+ results: [],
48
+ summary: { total: 0, passed: 0, failed: 0, withWarnings: 0 },
49
+ },
50
+ null,
51
+ 2
52
+ )
53
+ );
54
+ } else {
55
+ console.log(chalk.red(`${icons.failed} No scenario files found matching: ${pathArg}`));
56
+ }
57
+ process.exit(2);
58
+ }
59
+
60
+ // Validate all files
61
+ const results: ValidationResult[] = [];
62
+
63
+ if (!options.json && !options.quiet) {
64
+ console.log(chalk.bold('Validating scenarios...\n'));
65
+ }
66
+
67
+ for (const file of files) {
68
+ const result = validator.validate(file);
69
+ results.push(result);
70
+
71
+ // In strict mode, warnings become errors
72
+ if (options.strict && result.warnings.length > 0) {
73
+ result.valid = false;
74
+ result.errors.push(
75
+ ...result.warnings.map((w: ValidationResult['warnings'][0]) => ({
76
+ ...w,
77
+ severity: 'error' as const,
78
+ }))
79
+ );
80
+ }
81
+
82
+ if (!options.json) {
83
+ printFileResult(result, options);
84
+ }
85
+ }
86
+
87
+ // Calculate summary
88
+ const summary: ValidationSummary = {
89
+ total: results.length,
90
+ passed: results.filter((r) => r.valid && r.warnings.length === 0).length,
91
+ failed: results.filter((r) => !r.valid).length,
92
+ withWarnings: results.filter((r) => r.valid && r.warnings.length > 0).length,
93
+ };
94
+
95
+ // Output results
96
+ if (options.json) {
97
+ console.log(
98
+ JSON.stringify(
99
+ {
100
+ valid: summary.failed === 0,
101
+ results: results.map((r) => ({
102
+ file: r.file,
103
+ valid: r.valid,
104
+ errors: r.errors,
105
+ warnings: r.warnings,
106
+ })),
107
+ summary,
108
+ },
109
+ null,
110
+ 2
111
+ )
112
+ );
113
+ } else if (!options.quiet) {
114
+ console.log();
115
+ printSummary(summary, options.strict);
116
+ }
117
+
118
+ // Export to JUnit if requested
119
+ if (options.export === 'junit') {
120
+ const exportDir = options.exportOutput || './artemis-exports';
121
+ await mkdir(exportDir, { recursive: true });
122
+ const junit = generateValidationJUnitReport(results);
123
+ const junitPath = join(exportDir, `validation-${Date.now()}.xml`);
124
+ await writeFile(junitPath, junit);
125
+ if (!options.quiet) {
126
+ console.log(chalk.dim(`Exported: ${junitPath}`));
127
+ }
128
+ }
129
+
130
+ // Exit with appropriate code
131
+ if (summary.failed > 0) {
132
+ process.exit(1);
133
+ }
134
+ });
135
+
136
+ return cmd;
137
+ }
138
+
139
+ /**
140
+ * Resolve files from path argument (file, directory, or glob)
141
+ */
142
+ function resolveFiles(pathArg: string): string[] {
143
+ const resolved = resolve(pathArg);
144
+
145
+ try {
146
+ const stat = statSync(resolved);
147
+
148
+ if (stat.isFile()) {
149
+ // Single file
150
+ return [resolved];
151
+ }
152
+
153
+ if (stat.isDirectory()) {
154
+ // Directory - find all yaml files recursively
155
+ return findYamlFiles(resolved);
156
+ }
157
+ } catch {
158
+ // Path doesn't exist as file/directory - try as glob
159
+ }
160
+
161
+ // Try as glob pattern using Bun's Glob
162
+ const glob = new Glob(pathArg);
163
+ const matches: string[] = [];
164
+ for (const file of glob.scanSync({ absolute: true, onlyFiles: true })) {
165
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
166
+ matches.push(file);
167
+ }
168
+ }
169
+
170
+ return matches;
171
+ }
172
+
173
+ /**
174
+ * Find all YAML files in a directory recursively
175
+ */
176
+ function findYamlFiles(dir: string): string[] {
177
+ const files: string[] = [];
178
+
179
+ const entries = readdirSync(dir, { withFileTypes: true });
180
+
181
+ for (const entry of entries) {
182
+ const fullPath = join(dir, entry.name);
183
+
184
+ if (entry.isDirectory()) {
185
+ files.push(...findYamlFiles(fullPath));
186
+ } else if (entry.isFile() && (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml'))) {
187
+ files.push(fullPath);
188
+ }
189
+ }
190
+
191
+ return files;
192
+ }
193
+
194
+ /**
195
+ * Print result for a single file
196
+ */
197
+ function printFileResult(result: ValidationResult, options: ValidateOptions): void {
198
+ const fileName = basename(result.file);
199
+
200
+ if (result.valid && result.warnings.length === 0) {
201
+ if (!options.quiet) {
202
+ console.log(`${icons.passed} ${chalk.green(fileName)}`);
203
+ }
204
+ } else if (result.valid && result.warnings.length > 0) {
205
+ console.log(`${icons.warning} ${chalk.yellow(fileName)}`);
206
+ for (const warning of result.warnings) {
207
+ const location = warning.column
208
+ ? `Line ${warning.line}:${warning.column}`
209
+ : `Line ${warning.line}`;
210
+ console.log(chalk.yellow(` ${location}: ${warning.message}`));
211
+ if (warning.suggestion) {
212
+ console.log(chalk.dim(` Suggestion: ${warning.suggestion}`));
213
+ }
214
+ }
215
+ } else {
216
+ console.log(`${icons.failed} ${chalk.red(fileName)}`);
217
+ for (const error of result.errors) {
218
+ const location = error.column ? `Line ${error.line}:${error.column}` : `Line ${error.line}`;
219
+ console.log(chalk.red(` ${location}: ${error.message}`));
220
+ if (error.suggestion) {
221
+ console.log(chalk.dim(` Suggestion: ${error.suggestion}`));
222
+ }
223
+ }
224
+ for (const warning of result.warnings) {
225
+ const location = warning.column
226
+ ? `Line ${warning.line}:${warning.column}`
227
+ : `Line ${warning.line}`;
228
+ console.log(chalk.yellow(` ${location}: ${warning.message}`));
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Print validation summary
235
+ */
236
+ function printSummary(summary: ValidationSummary, strict?: boolean): void {
237
+ const parts: string[] = [];
238
+
239
+ if (summary.passed > 0) {
240
+ parts.push(chalk.green(`${summary.passed} passed`));
241
+ }
242
+ if (summary.failed > 0) {
243
+ parts.push(chalk.red(`${summary.failed} failed`));
244
+ }
245
+ if (summary.withWarnings > 0 && !strict) {
246
+ parts.push(chalk.yellow(`${summary.withWarnings} with warnings`));
247
+ }
248
+
249
+ const statusIcon = summary.failed > 0 ? icons.failed : icons.passed;
250
+ const statusColor = summary.failed > 0 ? chalk.red : chalk.green;
251
+
252
+ console.log(statusColor(`${statusIcon} ${parts.join(', ')}`));
253
+ console.log(chalk.dim(`${summary.total} scenario(s) validated`));
254
+ }
@@ -22,6 +22,12 @@ const ProviderConfigSchema = z.object({
22
22
  modelFamily: z.string().optional(),
23
23
  // Vercel AI specific
24
24
  underlyingProvider: z.enum(['openai', 'azure', 'anthropic', 'google', 'mistral']).optional(),
25
+ // LangChain specific
26
+ name: z.string().optional(),
27
+ runnableType: z.enum(['chain', 'agent', 'llm', 'runnable']).optional(),
28
+ // DeepAgents specific
29
+ captureTraces: z.boolean().optional(),
30
+ captureMessages: z.boolean().optional(),
25
31
  });
26
32
 
27
33
  const StorageConfigSchema = z.object({
@@ -119,6 +119,30 @@ export function buildAdapterConfig(options: AdapterConfigOptions): AdapterConfig
119
119
  fileProviderConfig,
120
120
  });
121
121
 
122
+ case 'langchain':
123
+ return buildLangChainConfig({
124
+ provider,
125
+ providerSource,
126
+ model,
127
+ modelSource,
128
+ temperature,
129
+ maxTokens,
130
+ scenarioConfig,
131
+ fileProviderConfig,
132
+ });
133
+
134
+ case 'deepagents':
135
+ return buildDeepAgentsConfig({
136
+ provider,
137
+ providerSource,
138
+ model,
139
+ modelSource,
140
+ temperature,
141
+ maxTokens,
142
+ scenarioConfig,
143
+ fileProviderConfig,
144
+ });
145
+
122
146
  default:
123
147
  // Fallback for unknown providers - treat as OpenAI-compatible
124
148
  return buildOpenAIConfig({
@@ -478,6 +502,149 @@ function buildAnthropicConfig(options: ProviderBuildOptions): AdapterConfigResul
478
502
  };
479
503
  }
480
504
 
505
+ function buildLangChainConfig(options: ProviderBuildOptions): AdapterConfigResult {
506
+ const {
507
+ provider,
508
+ providerSource,
509
+ model,
510
+ modelSource,
511
+ temperature,
512
+ maxTokens,
513
+ scenarioConfig,
514
+ fileProviderConfig,
515
+ } = options;
516
+
517
+ const resolvedModel = resolveValueWithSource<string>(
518
+ { value: model, source: modelSource },
519
+ { value: scenarioConfig?.defaultModel, source: 'scenario' },
520
+ { value: fileProviderConfig?.defaultModel, source: 'config' }
521
+ );
522
+
523
+ const resolvedName = resolveValueWithSource<string>(
524
+ { value: scenarioConfig?.name, source: 'scenario' },
525
+ { value: fileProviderConfig?.name, source: 'config' }
526
+ );
527
+
528
+ const resolvedRunnableType = resolveValueWithSource<string>(
529
+ { value: scenarioConfig?.runnableType, source: 'scenario' },
530
+ { value: fileProviderConfig?.runnableType, source: 'config' }
531
+ );
532
+
533
+ const resolvedTimeout = resolveValueWithSource<number>(
534
+ { value: scenarioConfig?.timeout, source: 'scenario' },
535
+ { value: fileProviderConfig?.timeout, source: 'config' }
536
+ );
537
+
538
+ // Temperature and maxTokens only come from CLI options
539
+ const resolvedTemperature = resolveValueWithSource<number>({ value: temperature, source: 'cli' });
540
+ const resolvedMaxTokens = resolveValueWithSource<number>({ value: maxTokens, source: 'cli' });
541
+
542
+ return {
543
+ adapterConfig: {
544
+ provider: 'langchain',
545
+ name: resolvedName.value,
546
+ runnableType: resolvedRunnableType.value as 'chain' | 'agent' | 'llm' | 'runnable',
547
+ defaultModel: resolvedModel.value,
548
+ timeout: resolvedTimeout.value,
549
+ },
550
+ resolvedConfig: {
551
+ provider,
552
+ model: resolvedModel.value,
553
+ name: resolvedName.value,
554
+ runnable_type: resolvedRunnableType.value,
555
+ timeout: resolvedTimeout.value,
556
+ temperature: resolvedTemperature.value,
557
+ max_tokens: resolvedMaxTokens.value,
558
+ source: {
559
+ provider: providerSource,
560
+ model: resolvedModel.source,
561
+ name: resolvedName.source,
562
+ runnable_type: resolvedRunnableType.source,
563
+ timeout: resolvedTimeout.source,
564
+ temperature: resolvedTemperature.source,
565
+ max_tokens: resolvedMaxTokens.source,
566
+ },
567
+ },
568
+ };
569
+ }
570
+
571
+ function buildDeepAgentsConfig(options: ProviderBuildOptions): AdapterConfigResult {
572
+ const {
573
+ provider,
574
+ providerSource,
575
+ model,
576
+ modelSource,
577
+ temperature,
578
+ maxTokens,
579
+ scenarioConfig,
580
+ fileProviderConfig,
581
+ } = options;
582
+
583
+ const resolvedModel = resolveValueWithSource<string>(
584
+ { value: model, source: modelSource },
585
+ { value: scenarioConfig?.defaultModel, source: 'scenario' },
586
+ { value: fileProviderConfig?.defaultModel, source: 'config' }
587
+ );
588
+
589
+ const resolvedName = resolveValueWithSource<string>(
590
+ { value: scenarioConfig?.name, source: 'scenario' },
591
+ { value: fileProviderConfig?.name, source: 'config' }
592
+ );
593
+
594
+ const resolvedTimeout = resolveValueWithSource<number>(
595
+ { value: scenarioConfig?.timeout, source: 'scenario' },
596
+ { value: fileProviderConfig?.timeout, source: 'config' },
597
+ { value: 300000, source: 'default' } // 5 minute default for multi-agent systems
598
+ );
599
+
600
+ const resolvedCaptureTraces = resolveValueWithSource<boolean>(
601
+ { value: scenarioConfig?.captureTraces, source: 'scenario' },
602
+ { value: fileProviderConfig?.captureTraces, source: 'config' },
603
+ { value: true, source: 'default' }
604
+ );
605
+
606
+ const resolvedCaptureMessages = resolveValueWithSource<boolean>(
607
+ { value: scenarioConfig?.captureMessages, source: 'scenario' },
608
+ { value: fileProviderConfig?.captureMessages, source: 'config' },
609
+ { value: true, source: 'default' }
610
+ );
611
+
612
+ // Temperature and maxTokens only come from CLI options
613
+ const resolvedTemperature = resolveValueWithSource<number>({ value: temperature, source: 'cli' });
614
+ const resolvedMaxTokens = resolveValueWithSource<number>({ value: maxTokens, source: 'cli' });
615
+
616
+ return {
617
+ adapterConfig: {
618
+ provider: 'deepagents',
619
+ name: resolvedName.value,
620
+ defaultModel: resolvedModel.value,
621
+ timeout: resolvedTimeout.value,
622
+ captureTraces: resolvedCaptureTraces.value,
623
+ captureMessages: resolvedCaptureMessages.value,
624
+ },
625
+ resolvedConfig: {
626
+ provider,
627
+ model: resolvedModel.value,
628
+ name: resolvedName.value,
629
+ timeout: resolvedTimeout.value,
630
+ capture_traces: resolvedCaptureTraces.value,
631
+ capture_messages: resolvedCaptureMessages.value,
632
+ temperature: resolvedTemperature.value,
633
+ max_tokens: resolvedMaxTokens.value,
634
+ source: {
635
+ provider: providerSource,
636
+ model: resolvedModel.source,
637
+ name: resolvedName.source,
638
+ timeout: resolvedTimeout.source,
639
+ capture_traces: resolvedCaptureTraces.source,
640
+ capture_messages: resolvedCaptureMessages.source,
641
+ temperature: resolvedTemperature.source,
642
+ max_tokens: resolvedMaxTokens.source,
643
+ },
644
+ },
645
+ };
646
+ }
647
+
481
648
  /**
482
649
  * Resolve a configuration value with source tracking
483
650
  * Returns the first defined (non-undefined) value and its source