mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-09-14 13:50:38 +08:00
242 lines
6.9 KiB
JavaScript
242 lines
6.9 KiB
JavaScript
import { existsSync, readFileSync, unwatchFile, watchFile } from 'fs';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
import * as v from 'valibot';
|
|
|
|
// Schema for individual agent configuration
|
|
const agent_config_schema = v.object({
|
|
model: v.pipe(
|
|
v.optional(v.string()),
|
|
v.description('Model identifier for the agent (e.g., "anthropic/claude-sonnet-4-20250514")'),
|
|
),
|
|
temperature: v.pipe(
|
|
v.optional(v.number()),
|
|
v.description('Temperature setting for the agent (e.g., 0.7)'),
|
|
),
|
|
top_p: v.pipe(
|
|
v.optional(v.number()),
|
|
v.description(
|
|
'Control response diversity with the top_p option. Alternative to temperature for controlling randomness.',
|
|
),
|
|
),
|
|
maxSteps: v.pipe(
|
|
v.optional(v.number()),
|
|
v.description('Maximum number of steps the agent can take (e.g., 10)'),
|
|
),
|
|
});
|
|
|
|
const default_config = {
|
|
mcp: {
|
|
type: /** @type {'remote' | 'local'} */ ('local'),
|
|
enabled: true,
|
|
},
|
|
subagent: {
|
|
enabled: true,
|
|
agents: /** @type {Record<string, v.InferInput<typeof agent_config_schema>>} */ ({}),
|
|
},
|
|
instructions: {
|
|
enabled: true,
|
|
},
|
|
skills: {
|
|
enabled: /** @type {boolean | string[]} */ (true),
|
|
},
|
|
autoupdate: true,
|
|
};
|
|
|
|
export const config_schema = v.object({
|
|
mcp: v.pipe(
|
|
v.optional(
|
|
v.object({
|
|
type: v.optional(v.picklist(['remote', 'local'])),
|
|
enabled: v.optional(v.boolean()),
|
|
}),
|
|
),
|
|
v.description(
|
|
"Configuration for the MCP. You can choose whether it is enabled and which transport to use: 'local' (default) or 'remote'.",
|
|
),
|
|
),
|
|
subagent: v.pipe(
|
|
v.optional(
|
|
v.object({
|
|
enabled: v.optional(v.boolean()),
|
|
agents: v.optional(v.record(v.string(), agent_config_schema)),
|
|
}),
|
|
),
|
|
v.description('Configuration for the subagent. You can choose if it should be enabled or not.'),
|
|
),
|
|
instructions: v.pipe(
|
|
v.optional(
|
|
v.object({
|
|
enabled: v.optional(v.boolean()),
|
|
}),
|
|
),
|
|
v.description(
|
|
'Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not.',
|
|
),
|
|
),
|
|
skills: v.pipe(
|
|
v.optional(
|
|
v.object({
|
|
enabled: v.pipe(
|
|
v.optional(v.union([v.boolean(), v.array(v.string())])),
|
|
v.description(
|
|
'It can be either a boolean or an array containing the skills that you want to enable',
|
|
),
|
|
),
|
|
}),
|
|
),
|
|
v.description(
|
|
'Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills.',
|
|
),
|
|
),
|
|
autoupdate: v.pipe(
|
|
v.optional(v.boolean()),
|
|
v.description(
|
|
'When a new version of an unpinned or latest-tagged plugin is available, remove it from the opencode cache on exit so that the latest version is installed the next time opencode starts. Enabled by default; set it to false to only get a warning.',
|
|
),
|
|
),
|
|
});
|
|
|
|
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
|
|
|
|
const GLOBAL_CONFIG_PATH = join(homedir(), '.config', 'opencode', 'svelte.json');
|
|
|
|
/** @typedef {{ data: Record<string, unknown> | null, parse_error?: string }} ConfigLoadResult */
|
|
/** @typedef {{ title: string, message: string }} ConfigWarning */
|
|
|
|
/** @param {string} directory */
|
|
function get_config_candidates(directory) {
|
|
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
|
|
// Lowest priority is first, so project config overrides global config.
|
|
return [
|
|
GLOBAL_CONFIG_PATH,
|
|
opencode_config_dir ? join(opencode_config_dir, 'svelte.json') : null,
|
|
join(directory, '.opencode', 'svelte.json'),
|
|
];
|
|
}
|
|
|
|
/** @param {string} directory */
|
|
function get_config_paths(directory) {
|
|
return get_config_candidates(directory).map((path) => (path && existsSync(path) ? path : null));
|
|
}
|
|
|
|
/**
|
|
* We watch for the config paths in the case of Opencode 2 because the plugin is only
|
|
* instantiated once per folder and then kept alive but the one long running opencode server
|
|
* this allows us to also hot reload in case the user changes the configuration with
|
|
* the opencode tui plugin.
|
|
* @param {string} directory
|
|
* @param {() => void} on_change
|
|
*/
|
|
export function watch_mcp_config(directory, on_change) {
|
|
const paths = [...new Set(get_config_candidates(directory).filter((path) => path !== null))];
|
|
for (const path of paths) watchFile(path, { interval: 500, persistent: false }, on_change);
|
|
return () => {
|
|
for (const path of paths) unwatchFile(path, on_change);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} config_path
|
|
* @returns {ConfigLoadResult}
|
|
*/
|
|
function load_config_file(config_path) {
|
|
/** @type {string} */
|
|
let file_content;
|
|
try {
|
|
file_content = readFileSync(config_path, 'utf-8');
|
|
} catch {
|
|
// File doesn't exist or can't be read
|
|
return { data: null };
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(file_content);
|
|
if (parsed === undefined || parsed === null) {
|
|
return { data: null, parse_error: 'Config file is empty or invalid' };
|
|
}
|
|
return { data: parsed };
|
|
} catch (error) {
|
|
return {
|
|
data: null,
|
|
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Partial<McpConfig>} user_config
|
|
* @returns {McpConfig}
|
|
*/
|
|
function merge_with_defaults(user_config) {
|
|
return {
|
|
mcp: {
|
|
...default_config.mcp,
|
|
...user_config.mcp,
|
|
},
|
|
subagent: {
|
|
enabled: default_config.subagent.enabled,
|
|
...user_config.subagent,
|
|
agents: {
|
|
...default_config.subagent.agents,
|
|
...user_config.subagent?.agents,
|
|
},
|
|
},
|
|
instructions: {
|
|
...default_config.instructions,
|
|
...user_config.instructions,
|
|
},
|
|
skills: {
|
|
...default_config.skills,
|
|
...user_config.skills,
|
|
},
|
|
autoupdate: user_config.autoupdate ?? default_config.autoupdate,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {{ directory?: string, on_warning?: (warning: ConfigWarning) => void }} [options]
|
|
*/
|
|
export function get_mcp_config(options = {}) {
|
|
const directory = options.directory ?? process.cwd();
|
|
const config_paths = get_config_paths(directory);
|
|
const on_warning = options.on_warning ?? (() => {});
|
|
/** @type {Partial<McpConfig>} */
|
|
let merged = {};
|
|
|
|
// Iterate from lowest to highest priority, merging as we go
|
|
for (const path of config_paths) {
|
|
if (path && existsSync(path)) {
|
|
const result = load_config_file(path);
|
|
if (result.parse_error) {
|
|
on_warning({
|
|
title: 'Svelte: Invalid OpenCode plugin config',
|
|
message: `${result.parse_error} (${path})\nSkipping this config file`,
|
|
});
|
|
continue;
|
|
}
|
|
const parsed = v.safeParse(config_schema, result.data);
|
|
if (parsed.success) {
|
|
merged = {
|
|
mcp: { ...merged.mcp, ...parsed.output.mcp },
|
|
subagent: {
|
|
...merged.subagent,
|
|
...parsed.output.subagent,
|
|
agents: { ...merged.subagent?.agents, ...parsed.output.subagent?.agents },
|
|
},
|
|
instructions: { ...merged.instructions, ...parsed.output.instructions },
|
|
skills: { ...merged.skills, ...parsed.output.skills },
|
|
autoupdate: parsed.output.autoupdate ?? merged.autoupdate,
|
|
};
|
|
} else {
|
|
on_warning({
|
|
title: 'Svelte: Invalid OpenCode plugin config',
|
|
message: `Invalid config schema (${path})\nSkipping this config file`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return merge_with_defaults(merged);
|
|
}
|