import * as vscode from 'vscode'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type AgentScope, type CommandScope, AGENT_SCOPE, COMMAND_SCOPE, discoverSkills, getSkillSources, createSkill, updateSkill, deleteSkill, readSkillSupportingFile, writeSkillSupportingFile, deleteSkillSupportingFile, type SkillScope, type DiscoveredSkill, SKILL_SCOPE, listMcpConfigs, getMcpConfig, createMcpConfig, updateMcpConfig, deleteMcpConfig, } from './opencodeConfig'; import { getSkillsCatalog, scanSkillsRepository as scanSkillsRepositoryFromGit, installSkillsFromRepository as installSkillsFromGit, type SkillsCatalogSourceConfig, } from './skillsCatalog'; import type { BridgeContext, BridgeResponse } from './bridge'; type BridgeMessageInput = { id: string; type: string; payload?: unknown; }; type ConfigRuntimeDeps = { readSettings: (ctx?: BridgeContext) => Record; persistSettings: (changes: Record, ctx?: BridgeContext) => Promise>; readMagicPromptOverrides: () => { version: number; overrides: Record }; saveMagicPromptOverride: (id: string, text: string) => Promise<{ version: number; overrides: Record }>; resetMagicPromptOverride: (id: string) => Promise<{ version: number; overrides: Record }>; resetAllMagicPromptOverrides: () => Promise<{ version: number; overrides: Record }>; fetchOpenCodeSkillsFromApi: (ctx: BridgeContext | undefined, workingDirectory?: string) => Promise; clientReloadDelayMs: number; }; const AGENTS_MD_PATH = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md'); const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024; const resolveWorkingDirectory = (ctx: BridgeContext | undefined, directory?: string): string | undefined => ( (typeof directory === 'string' && directory.trim()) ? directory.trim() : (ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath) ); const parseSkillsCatalogSources = (settings: Record): SkillsCatalogSourceConfig[] => { const rawCatalogs = (settings as { skillCatalogs?: unknown }).skillCatalogs; if (!Array.isArray(rawCatalogs)) { return []; } return rawCatalogs .map((entry) => { if (!entry || typeof entry !== 'object') return null; const candidate = entry as Record; const id = typeof candidate.id === 'string' ? candidate.id.trim() : ''; const label = typeof candidate.label === 'string' ? candidate.label.trim() : ''; const source = typeof candidate.source === 'string' ? candidate.source.trim() : ''; const subpath = typeof candidate.subpath === 'string' ? candidate.subpath.trim() : ''; if (!id || !label || !source) return null; const normalized: SkillsCatalogSourceConfig = { id, label, description: source, source, ...(subpath ? { defaultSubpath: subpath } : {}), }; return normalized; }) .filter((value): value is SkillsCatalogSourceConfig => value !== null); }; export async function handleConfigBridgeMessage( message: BridgeMessageInput, ctx: BridgeContext | undefined, deps: ConfigRuntimeDeps, ): Promise { const { id, type, payload } = message; switch (type) { case 'api:config/opencode-resolution:get': { const debugInfo = ctx?.manager?.getDebugInfo(); const configuredFromWorkspace = vscode.workspace.getConfiguration('openchamber').get('opencodeBinary'); const configured = typeof configuredFromWorkspace === 'string' && configuredFromWorkspace.trim().length > 0 ? configuredFromWorkspace.trim() : null; const resolved = debugInfo?.cliPath ?? null; const source = (() => { if (!resolved) return null; if (configured && configured === resolved) return 'settings'; const envBinary = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : ''; if (envBinary && envBinary === resolved) return 'env'; return 'path'; })(); return { id, type, success: true, data: { configured, resolved, resolvedDir: resolved ? path.dirname(resolved) : null, source, detectedNow: resolved, detectedSourceNow: source, shim: null, viaWsl: false, wslBinary: null, wslPath: null, wslDistro: null, node: process.execPath || null, bun: null, }, }; } case 'api:config/settings:get': { const settings = deps.readSettings(ctx); return { id, type, success: true, data: settings }; } case 'api:config/settings:save': { const changes = (payload as Record) || {}; const updated = await deps.persistSettings(changes, ctx); return { id, type, success: true, data: updated }; } case 'api:behavior/agents-md:get': { try { const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8'); return { id, type, success: true, data: { content, exists: true } }; } catch (error) { if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') { return { id, type, success: true, data: { content: '', exists: false } }; } throw error; } } case 'api:behavior/agents-md:save': { const request = (payload || {}) as { content?: unknown }; const content = typeof request.content === 'string' ? request.content : ''; if (content.length > MAX_BEHAVIOR_PROMPT_SIZE) { return { id, type, success: false, error: `Content exceeds maximum size of ${MAX_BEHAVIOR_PROMPT_SIZE} bytes` }; } await fs.promises.mkdir(path.dirname(AGENTS_MD_PATH), { recursive: true }); await fs.promises.writeFile(AGENTS_MD_PATH, content, 'utf8'); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true } }; } case 'api:magic-prompts:get': { return { id, type, success: true, data: deps.readMagicPromptOverrides() }; } case 'api:magic-prompts:save': { const request = (payload || {}) as { id?: string; text?: string }; const promptId = typeof request.id === 'string' ? request.id : ''; if (!promptId) { return { id, type, success: false, error: 'Prompt id is required' }; } if (typeof request.text !== 'string') { return { id, type, success: false, error: 'Prompt text is required' }; } const data = await deps.saveMagicPromptOverride(promptId, request.text); return { id, type, success: true, data }; } case 'api:magic-prompts:reset': { const request = (payload || {}) as { id?: string }; const promptId = typeof request.id === 'string' ? request.id : ''; if (!promptId) { return { id, type, success: false, error: 'Prompt id is required' }; } const data = await deps.resetMagicPromptOverride(promptId); return { id, type, success: true, data }; } case 'api:magic-prompts:reset-all': { const data = await deps.resetAllMagicPromptOverrides(); return { id, type, success: true, data }; } case 'api:config/reload': { await ctx?.manager?.restart(); return { id, type, success: true, data: { restarted: true } }; } case 'api:config/agents': { const { method, name, body, directory } = (payload || {}) as { method?: string; name?: string; body?: Record; directory?: string; }; const agentName = typeof name === 'string' ? name.trim() : ''; if (!agentName) { return { id, type, success: false, error: 'Agent name is required' }; } const workingDirectory = resolveWorkingDirectory(ctx, directory); const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET'; if (normalizedMethod === 'GET') { const sources = getAgentSources(agentName, workingDirectory); const scope = sources.md.exists ? sources.md.scope : (sources.json.exists ? sources.json.scope : null); return { id, type, success: true, data: { name: agentName, sources, scope, isBuiltIn: !sources.md.exists && !sources.json.exists }, }; } if (normalizedMethod === 'POST') { const scopeValue = body?.scope as string | undefined; const scope: AgentScope | undefined = scopeValue === 'project' ? AGENT_SCOPE.PROJECT : scopeValue === 'user' ? AGENT_SCOPE.USER : undefined; createAgent(agentName, (body || {}) as Record, workingDirectory, scope); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Agent ${agentName} created successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'PATCH') { updateAgent(agentName, (body || {}) as Record, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Agent ${agentName} updated successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'DELETE') { deleteAgent(agentName, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Agent ${agentName} deleted successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` }; } case 'api:config/commands': { const { method, name, body, directory } = (payload || {}) as { method?: string; name?: string; body?: Record; directory?: string; }; const commandName = typeof name === 'string' ? name.trim() : ''; if (!commandName) { return { id, type, success: false, error: 'Command name is required' }; } const workingDirectory = resolveWorkingDirectory(ctx, directory); const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET'; if (normalizedMethod === 'GET') { const sources = getCommandSources(commandName, workingDirectory); const scope = sources.md.exists ? sources.md.scope : (sources.json.exists ? sources.json.scope : null); return { id, type, success: true, data: { name: commandName, sources, scope, isBuiltIn: !sources.md.exists && !sources.json.exists }, }; } if (normalizedMethod === 'POST') { const scopeValue = body?.scope as string | undefined; const scope: CommandScope | undefined = scopeValue === 'project' ? COMMAND_SCOPE.PROJECT : scopeValue === 'user' ? COMMAND_SCOPE.USER : undefined; createCommand(commandName, (body || {}) as Record, workingDirectory, scope); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Command ${commandName} created successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'PATCH') { updateCommand(commandName, (body || {}) as Record, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Command ${commandName} updated successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'DELETE') { deleteCommand(commandName, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Command ${commandName} deleted successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` }; } case 'api:config/mcp': { const { method, name, body, directory } = (payload || {}) as { method?: string; name?: string; body?: Record; directory?: string; }; const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET'; const mcpName = typeof name === 'string' ? name.trim() : ''; const workingDirectory = resolveWorkingDirectory(ctx, directory); if (normalizedMethod === 'GET' && !mcpName) { const configs = listMcpConfigs(workingDirectory); return { id, type, success: true, data: configs }; } if (!mcpName) { return { id, type, success: false, error: 'MCP server name is required' }; } if (normalizedMethod === 'GET') { const config = getMcpConfig(mcpName, workingDirectory); if (!config) { return { id, type, success: false, error: `MCP server "${mcpName}" not found` }; } return { id, type, success: true, data: config }; } if (normalizedMethod === 'POST') { const scope = body?.scope as 'user' | 'project' | undefined; createMcpConfig(mcpName, (body || {}) as Record, workingDirectory, scope); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `MCP server "${mcpName}" created. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'PATCH') { updateMcpConfig(mcpName, (body || {}) as Record, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `MCP server "${mcpName}" updated. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'DELETE') { deleteMcpConfig(mcpName, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `MCP server "${mcpName}" deleted. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` }; } case 'api:config/skills': { const { method, name, body } = (payload || {}) as { method?: string; name?: string; body?: Record }; const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET'; if (!name && normalizedMethod === 'GET') { const skills = (await deps.fetchOpenCodeSkillsFromApi(ctx, workingDirectory)) || discoverSkills(workingDirectory); return { id, type, success: true, data: { skills } }; } const skillName = typeof name === 'string' ? name.trim() : ''; if (!skillName) { return { id, type, success: false, error: 'Skill name is required' }; } if (normalizedMethod === 'GET') { const discoveredSkill = ((await deps.fetchOpenCodeSkillsFromApi(ctx, workingDirectory)) || []) .find((skill) => skill.name === skillName); const sources = getSkillSources(skillName, workingDirectory, discoveredSkill || null); return { id, type, success: true, data: { name: skillName, sources, scope: sources.md.scope, source: sources.md.source }, }; } if (normalizedMethod === 'POST') { const scopeValue = body?.scope as string | undefined; const sourceValue = body?.source as string | undefined; const scope: SkillScope | undefined = scopeValue === 'project' ? SKILL_SCOPE.PROJECT : scopeValue === 'user' ? SKILL_SCOPE.USER : undefined; const normalizedSource = sourceValue === 'agents' ? 'agents' : 'opencode'; createSkill(skillName, { ...(body || {}), source: normalizedSource } as Record, workingDirectory, scope); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Skill ${skillName} created successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'PATCH') { updateSkill(skillName, (body || {}) as Record, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Skill ${skillName} updated successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } if (normalizedMethod === 'DELETE') { deleteSkill(skillName, workingDirectory); await ctx?.manager?.restart(); return { id, type, success: true, data: { success: true, requiresReload: true, message: `Skill ${skillName} deleted successfully. Reloading interface…`, reloadDelayMs: deps.clientReloadDelayMs, }, }; } return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` }; } case 'api:config/skills:catalog': { const refresh = Boolean((payload as { refresh?: boolean } | undefined)?.refresh); const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const settings = deps.readSettings(ctx); const additionalSources = parseSkillsCatalogSources(settings); const installedSkills = (await deps.fetchOpenCodeSkillsFromApi(ctx, workingDirectory)) || undefined; const data = await getSkillsCatalog(workingDirectory, refresh, additionalSources, installedSkills); return { id, type, success: true, data }; } case 'api:config/skills:scan': { const body = (payload || {}) as { source?: string; subpath?: string; gitIdentityId?: string }; const data = await scanSkillsRepositoryFromGit({ source: String(body.source || ''), subpath: body.subpath, }); return { id, type, success: true, data }; } case 'api:config/skills:install': { const body = (payload || {}) as { source?: string; subpath?: string; scope?: 'user' | 'project'; targetSource?: 'opencode' | 'agents'; selections?: Array<{ skillDir: string }>; conflictPolicy?: 'prompt' | 'skipAll' | 'overwriteAll'; conflictDecisions?: Record; }; const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const data = await installSkillsFromGit({ source: String(body.source || ''), subpath: body.subpath, scope: body.scope === 'project' ? 'project' : 'user', targetSource: body.targetSource === 'agents' ? 'agents' : 'opencode', workingDirectory: body.scope === 'project' ? workingDirectory : undefined, selections: Array.isArray(body.selections) ? body.selections : [], conflictPolicy: body.conflictPolicy, conflictDecisions: body.conflictDecisions, }); if (data.ok) { const installed = data.installed || []; const skipped = data.skipped || []; const requiresReload = installed.length > 0; if (requiresReload) { await ctx?.manager?.restart(); } return { id, type, success: true, data: { ok: true, installed, skipped, requiresReload, message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed', reloadDelayMs: requiresReload ? deps.clientReloadDelayMs : undefined, }, }; } return { id, type, success: true, data }; } case 'api:config/skills/files': { const { method, name, filePath, content } = (payload || {}) as { method?: string; name?: string; filePath?: string; content?: string; }; const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const skillName = typeof name === 'string' ? name.trim() : ''; if (!skillName) { return { id, type, success: false, error: 'Skill name is required' }; } const relativePath = typeof filePath === 'string' ? filePath.trim() : ''; if (!relativePath) { return { id, type, success: false, error: 'File path is required' }; } const discoveredSkill = ((await deps.fetchOpenCodeSkillsFromApi(ctx, workingDirectory)) || []) .find((skill) => skill.name === skillName); const sources = getSkillSources(skillName, workingDirectory, discoveredSkill || null); if (!sources.md.dir) { return { id, type, success: false, error: `Skill "${skillName}" not found` }; } const skillDir = sources.md.dir; const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET'; if (normalizedMethod === 'GET') { const fileContent = readSkillSupportingFile(skillDir, relativePath); if (fileContent === null) { return { id, type, success: false, error: `File "${relativePath}" not found in skill "${skillName}"` }; } return { id, type, success: true, data: { content: fileContent } }; } if (normalizedMethod === 'PUT') { writeSkillSupportingFile(skillDir, relativePath, content || ''); return { id, type, success: true, data: { success: true } }; } if (normalizedMethod === 'DELETE') { deleteSkillSupportingFile(skillDir, relativePath); return { id, type, success: true, data: { success: true } }; } return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` }; } default: return null; } }