@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
197 lines • 6.82 kB
JavaScript
import { TIMEOUT_PROVIDER_CONNECTION_MS } from '../constants.js';
import { isLocalURL } from '../utils/url-utils.js';
/**
* Validates the structure of the configuration object
*/
export function validateConfig(providers, mcpServers) {
const errors = [];
const warnings = [];
// Validate providers
if (providers.length === 0) {
warnings.push('No providers configured. Nanocoder requires at least one provider to function.');
}
for (const provider of providers) {
if (!provider.name) {
errors.push('Provider missing name');
}
if (!provider.models || provider.models.length === 0) {
errors.push(`Provider "${provider.name}" has no models configured`);
}
// Validate base URL if present
if (provider.baseUrl) {
try {
new URL(provider.baseUrl);
}
catch {
errors.push(`Provider "${provider.name}" has invalid base URL: ${provider.baseUrl}`);
}
}
}
// Validate MCP servers
for (const [name, server] of Object.entries(mcpServers)) {
if (!server.command) {
errors.push(`MCP server "${name}" missing command`);
}
if (!server.args) {
errors.push(`MCP server "${name}" missing args array`);
}
if (server.alwaysAllow && !Array.isArray(server.alwaysAllow)) {
errors.push(`MCP server "${name}" has invalid alwaysAllow (must be an array of strings)`);
}
if (Array.isArray(server.alwaysAllow)) {
const invalidItems = server.alwaysAllow.filter(item => typeof item !== 'string');
if (invalidItems.length > 0) {
errors.push(`MCP server "${name}" has non-string entries in alwaysAllow`);
}
}
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Tests connectivity to a provider
*/
export async function testProviderConnection(provider, timeout = TIMEOUT_PROVIDER_CONNECTION_MS) {
// If no base URL, assume it's valid (will be validated when actually connecting)
if (!provider.baseUrl) {
return {
providerName: provider.name,
connected: true,
};
}
try {
// Only test localhost connections (don't want to spam cloud APIs)
if (!isLocalURL(provider.baseUrl)) {
return {
providerName: provider.name,
connected: true, // Assume cloud APIs are reachable
};
}
// Test localhost connection with a simple fetch
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
const response = await fetch(provider.baseUrl, {
method: 'GET',
signal: controller.signal,
});
clearTimeout(timeoutId);
return {
providerName: provider.name,
connected: response.ok || response.status === 404, // 404 is ok, server is running
};
}
catch (error) {
return {
providerName: provider.name,
connected: false,
error: error instanceof Error
? error.message
: 'Unknown error testing connection',
};
}
}
/**
* Builds the provider configuration object for agents.config.json
*/
export function buildProviderConfigObject(providers) {
const config = {
nanocoder: {
providers: providers.map(p => {
const providerConfig = {
name: p.name,
models: p.models,
};
if (p.baseUrl) {
providerConfig.baseUrl = p.baseUrl;
}
if (p.apiKey) {
providerConfig.apiKey = p.apiKey;
}
if (p.organizationId) {
providerConfig.organizationId = p.organizationId;
}
if (p.timeout) {
providerConfig.timeout = p.timeout;
}
if (p.sdkProvider) {
providerConfig.sdkProvider = p.sdkProvider;
}
return providerConfig;
}),
},
};
return config;
}
/**
* Builds the MCP configuration object for .mcp.json
* Uses Claude Code object format (servers as object keys)
*/
export function buildMcpConfigObject(mcpServers) {
// Add enabled flag to all servers configured via wizard
const serversWithEnabled = {};
for (const [key, server] of Object.entries(mcpServers)) {
serversWithEnabled[key] = {
...server,
enabled: true, // Default to enabled for wizard configurations
};
}
return {
mcpServers: serversWithEnabled,
};
}
/**
* @deprecated Use buildProviderConfigObject and buildMcpConfigObject instead
* This function is kept for backward compatibility during migration
*/
export function buildConfigObject(providers, mcpServers) {
const config = {
nanocoder: {
providers: providers.map(p => {
const providerConfig = {
name: p.name,
models: p.models,
};
if (p.baseUrl) {
providerConfig.baseUrl = p.baseUrl;
}
if (p.apiKey) {
providerConfig.apiKey = p.apiKey;
}
if (p.organizationId) {
providerConfig.organizationId = p.organizationId;
}
if (p.timeout) {
providerConfig.timeout = p.timeout;
}
if (p.sdkProvider) {
providerConfig.sdkProvider = p.sdkProvider;
}
return providerConfig;
}),
},
};
// Add MCP servers if any - convert Record<string, McpServerConfig> to new array format
if (Object.keys(mcpServers).length > 0) {
config.nanocoder.mcpServers = Object.values(mcpServers).map(server => ({
name: server.name,
transport: server.transport || 'stdio', // Default to stdio for backward compatibility
command: server.command,
args: server.args,
env: server.env,
url: server.url,
headers: server.headers,
timeout: server.timeout,
alwaysAllow: server.alwaysAllow,
description: server.description,
tags: server.tags,
enabled: true, // Default to enabled for wizard configurations
}));
}
return config;
}
//# sourceMappingURL=validation.js.map