Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions source/agent-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { readFile } from 'fs/promises';
import { z } from 'zod';
import { createError } from 'error-causes';
import { ValidationError } from './ai-errors.js';
import { parseOpenCodeNDJSON } from './agent-parser.js';

/**
* Format Zod validation errors into a human-readable message.
* @param {any} zodError - Zod validation error
* @returns {string} Formatted error message
*/
export const formatZodError = (zodError) => {
const issues = zodError.issues || zodError.errors;
return issues
? issues.map(e => `${e.path.join('.')}: ${e.message}`).join('; ')
: zodError.message || 'Validation failed';
};

/**
* Get agent configuration based on agent name.
* Supports 'claude', 'opencode', and 'cursor' agents.
* All agents use their standard OAuth authentication flows.
* @param {string} agentName - Name of the agent ('claude', 'opencode', 'cursor')
* @returns {Object} Agent configuration with command and args
*/
export const getAgentConfig = (agentName = 'claude') => {
const agentConfigs = {
claude: {
command: 'claude',
args: ['-p', '--output-format', 'json', '--no-session-persistence']
},
opencode: {
command: 'opencode',
args: ['run', '--format', 'json'],
parseOutput: (stdout, logger) => parseOpenCodeNDJSON(stdout, logger)
},
cursor: {
command: 'agent',
args: ['--print', '--output-format', 'json', '--trust']
}
};

const config = agentConfigs[agentName.toLowerCase()];
if (!config) {
throw createError({
...ValidationError,
message: `Unknown agent: ${agentName}. Supported agents: ${Object.keys(agentConfigs).join(', ')}`
});
}

return config;
};

// YAGNI: only command + args — parseOutput is a runtime function, not a serializable config field
const agentConfigFileSchema = z.object({
command: z.string().min(1, { error: 'command is required' }),
args: z.array(z.string()).default([])
});

const readAgentConfigFile = async ({ configPath }) => {
try {
return await readFile(configPath, 'utf-8');
} catch (err) {
throw createError({
...ValidationError,
message: `Failed to read agent config file: ${configPath}`,
code: 'AGENT_CONFIG_READ_ERROR',
cause: err
});
Comment on lines +64 to +69

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: These should use custom error types so handlers can automatically route them individually using a handleErrors pattern match. This is probably fine as-is, but we should write an error-causes skill so agents know how to properly use this library.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference - partially addressed in #415

}
};

const parseJson = ({ configPath, raw }) => {
try {
return JSON.parse(raw);
} catch (err) {
throw createError({
...ValidationError,
message: `Agent config file is not valid JSON: ${configPath}`,
code: 'AGENT_CONFIG_PARSE_ERROR',
cause: err
});
}
};

const validateAgentConfig = (parsed) => {
try {
return agentConfigFileSchema.parse(parsed);
} catch (zodError) {
throw createError({
...ValidationError,
message: `Invalid agent config: ${formatZodError(zodError)}`,
code: 'AGENT_CONFIG_VALIDATION_ERROR',
cause: zodError
});
}
};

/**
* Load and validate an agent configuration from a JSON file.
* @param {string} configPath - Path to the JSON config file
* @returns {Promise<Object>} Validated agent config with command and args
*/
export const loadAgentConfig = async (configPath) => {
const raw = await readAgentConfigFile({ configPath });
const parsed = parseJson({ configPath, raw });
return validateAgentConfig(parsed);
};
Loading