agentnet 0.0.2 → 0.0.4

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,350 @@
1
+ import { jest } from '@jest/globals'
2
+
3
+ import { Agent } from '../agent/agent';
4
+ import { ConfigurationError, CompilationError } from '../errors';
5
+
6
+ // Mock the AgentRuntime module
7
+ jest.mock('../agent/runtime.js', () => ({
8
+ AgentRuntime: jest.fn(),
9
+ }));
10
+ // Import the mocked AgentRuntime to allow inspection (e.g. toHaveBeenCalledWith)
11
+ import { AgentRuntime } from '../agent/runtime.js';
12
+
13
+ describe('Agent Core Functionality', () => {
14
+ let agentBuilder;
15
+ let mockLlmApi;
16
+ let mockStoreInstance;
17
+ let mockIoInstance;
18
+
19
+ beforeEach(() => {
20
+ // Reset mocks before each test
21
+ jest.clearAllMocks();
22
+
23
+ agentBuilder = Agent();
24
+ mockLlmApi = {
25
+ getClient: jest.fn().mockResolvedValue({}),
26
+ callModel: jest.fn().mockResolvedValue('llm_response'),
27
+ // Adding mock prompt and onResponse as AgentRuntime might expect them if not deeply mocked
28
+ prompt: jest.fn(),
29
+ onResponse: jest.fn(),
30
+ };
31
+ mockStoreInstance = {
32
+ connect: jest.fn().mockResolvedValue(true),
33
+ // Add other methods if your store interactions become more complex in tests
34
+ };
35
+ mockIoInstance = {
36
+ type: 'TestIO',
37
+ // Mock other IO methods if needed by AgentRuntime or specific IO logic
38
+ };
39
+ });
40
+
41
+ describe('Agent Factory & Defaults', () => {
42
+ it('should create a new agent builder with default metadata', () => {
43
+ expect(agentBuilder._config.metadata).toEqual({
44
+ name: 'default',
45
+ namespace: 'default',
46
+ description: 'A default agent',
47
+ });
48
+ });
49
+
50
+ it('should create a new agent builder with default runner config', () => {
51
+ expect(agentBuilder._config.runner).toEqual({
52
+ maxRuns: 10,
53
+ });
54
+ });
55
+
56
+ it('should create a new agent builder with default hooks', () => {
57
+ expect(typeof agentBuilder._config.on.prompt).toBe('function');
58
+ expect(typeof agentBuilder._config.on.response).toBe('function');
59
+ // Test the default behavior of hooks
60
+ const testInput = "test input";
61
+ expect(agentBuilder._config.on.prompt({}, testInput)).resolves.toBe(testInput);
62
+ const testResult = "test result";
63
+ expect(agentBuilder._config.on.response({}, [], testResult)).resolves.toBe(testResult);
64
+ });
65
+ });
66
+
67
+ describe('setMetadata(metadata)', () => {
68
+ it('should allow setting valid metadata', () => {
69
+ const metadata = { name: 'testAgent', namespace: 'testSpace', description: 'A test agent' };
70
+ agentBuilder.setMetadata(metadata);
71
+ expect(agentBuilder._config.metadata).toEqual(metadata);
72
+ });
73
+
74
+ it('should merge with existing metadata, overwriting common fields', () => {
75
+ agentBuilder.setMetadata({ name: 'initialName', customField: 'initialValue' });
76
+ agentBuilder.setMetadata({ name: 'newName', description: 'newDescription' });
77
+ expect(agentBuilder._config.metadata).toEqual({
78
+ name: 'newName',
79
+ namespace: 'default', // From initial defaults if not overridden
80
+ description: 'newDescription',
81
+ customField: 'initialValue'
82
+ });
83
+ });
84
+
85
+ it('should throw ConfigurationError if metadata is null', () => {
86
+ expect(() => agentBuilder.setMetadata(null)).toThrow(ConfigurationError);
87
+ expect(() => agentBuilder.setMetadata(null)).toThrow('Metadata is required');
88
+ });
89
+
90
+ // Validations for name and namespace during compile are more prominent,
91
+ // but direct setters could also enforce this, though current code doesn't.
92
+ // If direct enforcement in setMetadata is desired, add tests here.
93
+ // For now, these are primarily tested via compile's validation.
94
+ });
95
+
96
+ describe('withLLM(llmApi, llmConfig)', () => {
97
+ it('should configure LLM with valid API and config', () => {
98
+ const llmConfig = { model: 'test-model', temperature: 0.7 };
99
+ agentBuilder.withLLM(mockLlmApi, llmConfig);
100
+ expect(agentBuilder._config.llm.api).toBe(mockLlmApi);
101
+ expect(agentBuilder._config.llm.config).toEqual(llmConfig);
102
+ });
103
+
104
+ it('should throw ConfigurationError if llmApi is null', () => {
105
+ expect(() => agentBuilder.withLLM(null, {})).toThrow(ConfigurationError);
106
+ expect(() => agentBuilder.withLLM(null, {})).toThrow('LLM API is required');
107
+ });
108
+
109
+ it('should throw ConfigurationError if llmApi is not an object (during compile)', async () => {
110
+ agentBuilder.withLLM("notAnObject", {});
111
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("LLM API must be a valid object"));
112
+ });
113
+
114
+ it('should throw ConfigurationError if llmApi is missing getClient (during compile)', async () => {
115
+ const invalidApi = { ...mockLlmApi };
116
+ delete invalidApi.getClient;
117
+ agentBuilder.withLLM(invalidApi, {});
118
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("LLM API must have a getClient method"));
119
+ });
120
+
121
+ it('should throw ConfigurationError if llmApi is missing callModel (during compile)', async () => {
122
+ const invalidApi = { ...mockLlmApi };
123
+ delete invalidApi.callModel;
124
+ agentBuilder.withLLM(invalidApi, {});
125
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("LLM API must have a callModel method"));
126
+ });
127
+ });
128
+
129
+ describe('withStore(storeInstance, storeConfig)', () => {
130
+ it('should configure store with valid instance and config', () => {
131
+ const storeConfig = { type: 'test-store' };
132
+ agentBuilder.withStore(mockStoreInstance, storeConfig);
133
+ expect(agentBuilder._config.store.instance).toBe(mockStoreInstance);
134
+ expect(agentBuilder._config.store.config).toEqual(storeConfig);
135
+ });
136
+
137
+ it('should throw ConfigurationError if storeInstance is null', () => {
138
+ expect(() => agentBuilder.withStore(null, {})).toThrow(ConfigurationError);
139
+ expect(() => agentBuilder.withStore(null, {})).toThrow('Store instance is required');
140
+ });
141
+
142
+ it('should throw ConfigurationError if storeInstance is not an object (during compile)', async () => {
143
+ agentBuilder.withLLM(mockLlmApi, {}); // Need LLM for compilation
144
+ agentBuilder.withStore("notAnObject", {});
145
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Store instance must be a valid object"));
146
+ });
147
+
148
+ it('should throw ConfigurationError if storeInstance is missing connect (during compile)', async () => {
149
+ const invalidStore = {}; // Missing connect
150
+ agentBuilder.withLLM(mockLlmApi, {});
151
+ agentBuilder.withStore(invalidStore, {});
152
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Store instance must have a connect method"));
153
+ });
154
+ });
155
+
156
+ describe('addIO(instance, ioConfig)', () => {
157
+ it('should add IO interface with valid instance and config', () => {
158
+ const ioConfig = { setting: 'test-setting' };
159
+ agentBuilder.addIO(mockIoInstance, ioConfig);
160
+ expect(agentBuilder._config.io[0].type).toBe(mockIoInstance.type);
161
+ expect(agentBuilder._config.io[0].instance).toBe(mockIoInstance);
162
+ expect(agentBuilder._config.io[0].config).toEqual(ioConfig);
163
+ });
164
+
165
+ it('should throw ConfigurationError if instance is null', () => {
166
+ expect(() => agentBuilder.addIO(null, {})).toThrow(ConfigurationError);
167
+ expect(() => agentBuilder.addIO(null, {})).toThrow('IO instance must have a type');
168
+ });
169
+
170
+ it('should throw ConfigurationError if instance.type is missing', () => {
171
+ expect(() => agentBuilder.addIO({}, {})).toThrow(ConfigurationError);
172
+ expect(() => agentBuilder.addIO({}, {})).toThrow('IO instance must have a type');
173
+ });
174
+
175
+ // Further IO validation (missing instance/config on compile) is tested in compile section
176
+ });
177
+
178
+ describe('addToolSchema(schema)', () => {
179
+ it('should add a valid tool schema', () => {
180
+ const toolSchema = { name: 'testTool', description: 'A tool for testing' };
181
+ agentBuilder.addToolSchema(toolSchema);
182
+ expect(agentBuilder._config.toolsSchemas['testTool']).toEqual(toolSchema);
183
+ });
184
+
185
+ it('should throw ConfigurationError if schema is null', () => {
186
+ expect(() => agentBuilder.addToolSchema(null)).toThrow(ConfigurationError);
187
+ expect(() => agentBuilder.addToolSchema(null)).toThrow('Tool schema must have a name');
188
+
189
+ });
190
+
191
+ it('should throw ConfigurationError if schema.name is missing', () => {
192
+ expect(() => agentBuilder.addToolSchema({ description: ' nameless tool' })).toThrow(ConfigurationError);
193
+ expect(() => agentBuilder.addToolSchema({ description: ' nameless tool' })).toThrow('Tool schema must have a name');
194
+ });
195
+ });
196
+
197
+ describe('addDiscoverySchema(schema)', () => {
198
+ it('should add a valid discovery schema', () => {
199
+ const discoverySchema = { name: 'testDiscovery', description: 'For discovering things' };
200
+ agentBuilder.addDiscoverySchema(discoverySchema);
201
+ expect(agentBuilder._config.discoverySchemas).toContainEqual(discoverySchema);
202
+ });
203
+
204
+ it('should throw ConfigurationError if schema is null', () => {
205
+ expect(() => agentBuilder.addDiscoverySchema(null)).toThrow(ConfigurationError);
206
+ expect(() => agentBuilder.addDiscoverySchema(null)).toThrow('Discovery schema is required');
207
+ });
208
+ });
209
+
210
+ describe('on(eventName, handler)', () => {
211
+ it('should register custom prompt and response handlers', () => {
212
+ const mockPromptFn = jest.fn();
213
+ const mockResponseFn = jest.fn();
214
+ agentBuilder.on('prompt', mockPromptFn);
215
+ agentBuilder.on('response', mockResponseFn);
216
+ expect(agentBuilder._config.on.prompt).toBe(mockPromptFn);
217
+ expect(agentBuilder._config.on.response).toBe(mockResponseFn);
218
+ });
219
+
220
+ it('should throw ConfigurationError if handler is not a function', () => {
221
+ expect(() => agentBuilder.on('prompt', 'not-a-function')).toThrow(ConfigurationError);
222
+ expect(() => agentBuilder.on('prompt', 'not-a-function')).toThrow('Event handler for prompt must be a function');
223
+ });
224
+ });
225
+
226
+ describe('getToolsSchemas()', () => {
227
+ it('should return a copy of tool schemas', () => {
228
+ const toolSchema1 = { name: 'tool1', description: 'Tool one' };
229
+ const toolSchema2 = { name: 'tool2', description: 'Tool two' };
230
+ agentBuilder.addToolSchema(toolSchema1);
231
+ agentBuilder.addToolSchema(toolSchema2);
232
+
233
+ const retrievedSchemas = agentBuilder.getToolsSchemas();
234
+ expect(retrievedSchemas).toEqual({ tool1: toolSchema1, tool2: toolSchema2 });
235
+ // Ensure it's a copy
236
+ retrievedSchemas.tool1.description = "modified";
237
+ expect(agentBuilder._config.toolsSchemas.tool1.description).toBe("Tool one");
238
+ });
239
+
240
+ it('should return an empty object if no tools are added', () => {
241
+ expect(agentBuilder.getToolsSchemas()).toEqual({});
242
+ });
243
+ });
244
+
245
+ describe('compile()', () => {
246
+ beforeEach(() => {
247
+ // Minimum valid config for most compile tests
248
+ agentBuilder.withLLM(mockLlmApi, { model: 'test-model' });
249
+ agentBuilder.setMetadata({ name: 'compileAgent', namespace: 'compileSpace' });
250
+ });
251
+
252
+ it('should successfully compile with minimal valid configuration', async () => {
253
+ AgentRuntime.mockResolvedValue({ query: jest.fn() });
254
+ const compiledAgent = await agentBuilder.compile();
255
+ expect(AgentRuntime).toHaveBeenCalledTimes(1);
256
+ expect(AgentRuntime).toHaveBeenCalledWith(agentBuilder._config); // Check if called with the correct config
257
+ expect(compiledAgent).toBeDefined();
258
+ expect(typeof compiledAgent.query).toBe('function');
259
+ });
260
+
261
+ it('should throw ConfigurationError if metadata.name is empty during compile', async () => {
262
+ agentBuilder.setMetadata({ name: '', namespace: 'test' });
263
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Agent name cannot be empty"));
264
+ });
265
+
266
+ it('should throw ConfigurationError if metadata.namespace is empty during compile', async () => {
267
+ agentBuilder.setMetadata({ name: 'test', namespace: ' ' }); // Whitespace
268
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Agent namespace cannot be empty"));
269
+ });
270
+
271
+ it('should throw ConfigurationError if LLM is not configured', async () => {
272
+ const freshAgent = Agent(); // No LLM
273
+ freshAgent.setMetadata({ name: 'noLlmAgent', namespace: 'test' });
274
+ await expect(freshAgent.compile()).rejects.toThrow(ConfigurationError);
275
+ // The schema validation might throw a generic "is required" or a more specific one based on schema order
276
+ // For AGENT_CONFIG_SCHEMA, 'llm' is required.
277
+ });
278
+
279
+ it('should throw ConfigurationError if an added IO interface has no instance (during compile)', async () => {
280
+ agentBuilder._config.io.push({ type: 'BadIO', config: {} /* no instance */});
281
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("IO interface BadIO at index 0 has no instance"));
282
+ });
283
+
284
+ it('should throw ConfigurationError if an added IO interface has no config (during compile)', async () => {
285
+ agentBuilder._config.io.push({ type: 'BadIO', instance: mockIoInstance /* no config */});
286
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("IO interface BadIO at index 0 has no configuration"));
287
+ });
288
+
289
+ it('should throw ConfigurationError if a tool schema is invalid (e.g., name missing, checked during compile)', async () => {
290
+ // Note: addToolSchema checks this, but validateConfiguration re-checks.
291
+ // This test ensures validateConfiguration's check works.
292
+ agentBuilder._config.toolsSchemas['badTool'] = { description: "I am bad" }; // No name in schema value
293
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Tool schema must have a name"));
294
+ });
295
+
296
+ it('should throw ConfigurationError if runner.maxRuns is not a positive number', async () => {
297
+ agentBuilder._config.runner.maxRuns = 0;
298
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("runner.maxRuns must be greater than 0"));
299
+ agentBuilder._config.runner.maxRuns = -1;
300
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("runner.maxRuns must be greater than 0"));
301
+ agentBuilder._config.runner.maxRuns = 'not a number';
302
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Invalid type for runner.maxRuns in agent_config: expected number, got string"));
303
+ });
304
+
305
+ it('should throw ConfigurationError if an event handler is not a function', async () => {
306
+ agentBuilder._config.on.prompt = "not a function";
307
+ await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Event handler for 'prompt' must be a function"));
308
+ });
309
+
310
+ it('should throw CompilationError if AgentRuntime throws an error', async () => {
311
+ const runtimeError = new Error('Runtime failed!');
312
+ AgentRuntime.mockRejectedValue(runtimeError);
313
+ await expect(agentBuilder.compile()).rejects.toThrow(CompilationError);
314
+ await expect(agentBuilder.compile()).rejects.toThrow(`Failed to compile agent ${agentBuilder._config.metadata.name}: ${runtimeError.message}`);
315
+ });
316
+
317
+ it('should pass full configuration to AgentRuntime', async () => {
318
+ const toolSchema = { name: 'myTool', parameters: {} };
319
+ const discoverySchema = { name: 'myDiscovery' };
320
+ const ioConfig = { network: 'testNet' };
321
+ const storeConfig = { db: 'testDb' };
322
+ const promptHook = jest.fn();
323
+ const responseHook = jest.fn();
324
+
325
+ agentBuilder
326
+ .addToolSchema(toolSchema)
327
+ .addDiscoverySchema(discoverySchema)
328
+ .addIO(mockIoInstance, ioConfig)
329
+ .withStore(mockStoreInstance, storeConfig)
330
+ .on('prompt', promptHook)
331
+ .on('response', responseHook);
332
+
333
+ AgentRuntime.mockResolvedValue({ query: jest.fn() });
334
+ await agentBuilder.compile();
335
+
336
+ expect(AgentRuntime).toHaveBeenCalledWith(
337
+ expect.objectContaining({
338
+ metadata: agentBuilder._config.metadata,
339
+ llm: agentBuilder._config.llm,
340
+ store: agentBuilder._config.store,
341
+ io: expect.arrayContaining([expect.objectContaining({ type: mockIoInstance.type, instance: mockIoInstance, config: ioConfig })]),
342
+ toolsSchemas: expect.objectContaining({ 'myTool': toolSchema }),
343
+ discoverySchemas: expect.arrayContaining([discoverySchema]),
344
+ on: expect.objectContaining({ prompt: promptHook, response: responseHook }),
345
+ runner: agentBuilder._config.runner
346
+ })
347
+ );
348
+ });
349
+ });
350
+ });
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { parse, stringify } from 'yaml';
6
+ import { migrateDefinition, validateApiVersion, API_VERSIONS, LATEST_STABLE_VERSION } from '../utils/version.js';
7
+ import { logger } from '../utils/logger.js';
8
+
9
+ /**
10
+ * Print command usage instructions
11
+ */
12
+ function printUsage() {
13
+ console.log(`
14
+ Agent Definition Version Migration Utility
15
+
16
+ Usage:
17
+ node migrate-version.js <input-file> [options]
18
+
19
+ Options:
20
+ --output <file> Output file (default: adds '-migrated' to input filename)
21
+ --version <ver> Target API version (default: ${LATEST_STABLE_VERSION})
22
+ --check Only check if migration is needed, don't perform it
23
+ --quiet Suppress informational output
24
+ --help Show this help message
25
+
26
+ Examples:
27
+ node migrate-version.js ./agents.yaml
28
+ node migrate-version.js ./agents.yaml --version agentnet.io/v1alpha1
29
+ node migrate-version.js ./agents.yaml --output ./agents-new.yaml
30
+ node migrate-version.js ./agents.yaml --check
31
+ `);
32
+ }
33
+
34
+ /**
35
+ * Parse command line arguments
36
+ * @returns {Object} Parsed arguments
37
+ */
38
+ function parseArgs() {
39
+ const args = process.argv.slice(2);
40
+ const result = {
41
+ inputFile: null,
42
+ outputFile: null,
43
+ targetVersion: LATEST_STABLE_VERSION,
44
+ checkOnly: false,
45
+ quiet: false,
46
+ help: false
47
+ };
48
+
49
+ for (let i = 0; i < args.length; i++) {
50
+ const arg = args[i];
51
+
52
+ if (arg === '--help' || arg === '-h') {
53
+ result.help = true;
54
+ } else if (arg === '--output' || arg === '-o') {
55
+ result.outputFile = args[++i];
56
+ } else if (arg === '--version' || arg === '-v') {
57
+ result.targetVersion = args[++i];
58
+ } else if (arg === '--check' || arg === '-c') {
59
+ result.checkOnly = true;
60
+ } else if (arg === '--quiet' || arg === '-q') {
61
+ result.quiet = true;
62
+ } else if (!result.inputFile) {
63
+ result.inputFile = arg;
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Get default output filename based on input filename
72
+ * @param {string} inputFile - Input file path
73
+ * @param {string} targetVersion - Target API version
74
+ * @returns {string} Default output file path
75
+ */
76
+ function getDefaultOutputFile(inputFile, targetVersion) {
77
+ const parsedPath = path.parse(inputFile);
78
+ const versionSuffix = targetVersion.replace(/\//g, '-');
79
+ return path.join(
80
+ parsedPath.dir,
81
+ `${parsedPath.name}-${versionSuffix}${parsedPath.ext}`
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Log a message if not in quiet mode
87
+ * @param {string} message - Message to log
88
+ * @param {boolean} isError - Whether this is an error message
89
+ * @param {boolean} quiet - Whether quiet mode is enabled
90
+ */
91
+ function log(message, isError = false, quiet = false) {
92
+ if (isError || !quiet) {
93
+ console.log(message);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Process and migrate a YAML file containing agent definitions
99
+ * @param {string} content - YAML content
100
+ * @param {string} targetVersion - Target API version
101
+ * @param {boolean} checkOnly - Only check, don't modify
102
+ * @returns {Object} Migration results including modified content
103
+ */
104
+ function processYamlFile(content, targetVersion, checkOnly) {
105
+ // Split the YAML content by document separator
106
+ const documents = content.split(/^---$/m)
107
+ .map(s => s.trim())
108
+ .filter(s => s);
109
+
110
+ const result = {
111
+ migrated: false,
112
+ migratedCount: 0,
113
+ alreadyUpToDateCount: 0,
114
+ failedCount: 0,
115
+ needsMigration: false,
116
+ updatedContent: null,
117
+ failures: []
118
+ };
119
+
120
+ // Process each document
121
+ const processedDocs = [];
122
+
123
+ for (let i = 0; i < documents.length; i++) {
124
+ try {
125
+ const docContent = documents[i];
126
+ const doc = parse(docContent);
127
+
128
+ // Skip non-agent definitions
129
+ if (!doc || doc.kind !== 'AgentDefinition') {
130
+ processedDocs.push(docContent);
131
+ continue;
132
+ }
133
+
134
+ // Check if migration is needed
135
+ const currentVersion = doc.apiVersion || 'smartagent.io/v1alpha1';
136
+ const agentName = doc.metadata?.name || `[Document ${i+1}]`;
137
+
138
+ if (currentVersion === targetVersion) {
139
+ result.alreadyUpToDateCount++;
140
+ log(`Agent "${agentName}" is already at version ${targetVersion}`);
141
+ processedDocs.push(docContent);
142
+ continue;
143
+ }
144
+
145
+ result.needsMigration = true;
146
+
147
+ // If only checking, skip migration
148
+ if (checkOnly) {
149
+ processedDocs.push(docContent);
150
+ continue;
151
+ }
152
+
153
+ // Perform migration
154
+ const migratedDoc = migrateDefinition(doc, targetVersion);
155
+ result.migratedCount++;
156
+ result.migrated = true;
157
+
158
+ // Convert back to YAML and add to processed docs
159
+ processedDocs.push(stringify(migratedDoc));
160
+
161
+ log(`Successfully migrated agent "${agentName}" from ${currentVersion} to ${targetVersion}`);
162
+
163
+ } catch (error) {
164
+ result.failedCount++;
165
+ result.failures.push({
166
+ documentIndex: i,
167
+ error: error.message
168
+ });
169
+
170
+ // Keep original document on failure
171
+ processedDocs.push(documents[i]);
172
+
173
+ log(`Error processing document ${i+1}: ${error.message}`, true);
174
+ }
175
+ }
176
+
177
+ // Combine the processed documents back into a single YAML file
178
+ result.updatedContent = processedDocs.join('\n---\n');
179
+
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * Main function
185
+ */
186
+ async function main() {
187
+ const args = parseArgs();
188
+
189
+ // Show help if requested or no input file
190
+ if (args.help || !args.inputFile) {
191
+ printUsage();
192
+ process.exit(args.help ? 0 : 1);
193
+ }
194
+
195
+ // Validate target version
196
+ if (!API_VERSIONS[args.targetVersion]) {
197
+ log(`Error: Unsupported target version: ${args.targetVersion}`, true);
198
+ log(`Supported versions: ${Object.keys(API_VERSIONS).join(', ')}`, true);
199
+ process.exit(1);
200
+ }
201
+
202
+ try {
203
+ // Set default output file if not specified
204
+ if (!args.outputFile && !args.checkOnly) {
205
+ args.outputFile = getDefaultOutputFile(args.inputFile, args.targetVersion);
206
+ }
207
+
208
+ // Read input file
209
+ const content = fs.readFileSync(args.inputFile, 'utf8');
210
+
211
+ // Process the file
212
+ const result = processYamlFile(content, args.targetVersion, args.checkOnly);
213
+
214
+ // Output results
215
+ if (result.needsMigration) {
216
+ if (args.checkOnly) {
217
+ log(`Migration needed: ${result.alreadyUpToDateCount} up-to-date, ${result.failedCount + documents.length - result.alreadyUpToDateCount} need migration`);
218
+ process.exit(10); // Special exit code indicating migration needed
219
+ } else if (result.migrated) {
220
+ // Write the output file
221
+ fs.writeFileSync(args.outputFile, result.updatedContent);
222
+
223
+ log(`
224
+ Migration completed:
225
+ - ${result.migratedCount} agent definitions migrated
226
+ - ${result.alreadyUpToDateCount} already up-to-date
227
+ - ${result.failedCount} failures
228
+ - Output written to: ${args.outputFile}
229
+ `);
230
+ }
231
+ } else {
232
+ log(`No migration needed. All agent definitions are already at version ${args.targetVersion}`);
233
+ }
234
+
235
+ // Exit with error if any migrations failed
236
+ if (result.failedCount > 0) {
237
+ process.exit(1);
238
+ }
239
+
240
+ } catch (error) {
241
+ log(`Error: ${error.message}`, true);
242
+ process.exit(1);
243
+ }
244
+ }
245
+
246
+ // Run the main function
247
+ main().catch(error => {
248
+ console.error('Unhandled error:', error);
249
+ process.exit(1);
250
+ });