export const registerConfigEntityRoutes = (app, dependencies) => { const { resolveProjectDirectory, resolveOptionalProjectDirectory, refreshOpenCodeAfterConfigChange, clientReloadDelayMs, getAgentSources, getAgentConfig, createAgent, updateAgent, deleteAgent, getCommandSources, createCommand, updateCommand, deleteCommand, listMcpConfigs, getMcpConfig, createMcpConfig, updateMcpConfig, deleteMcpConfig, listSnippets, getSnippet, createSnippet, updateSnippet, deleteSnippet, expandSnippets, } = dependencies; // Build the response for a config mutation based on whether OpenCode actually // reloaded the change. When connected to an external OpenCode server that // OpenChamber cannot restart, the change is persisted to disk but the running // server will not serve it until the user restarts that server. We must not // report a clean "reloading" success in that case, otherwise the UI silently // reverts the edit to the stale value on the next refresh. const buildConfigMutationResponse = (refreshResult, { liveMessage, manualRestartMessage }) => { if (refreshResult && refreshResult.external) { return { success: true, requiresReload: false, requiresManualRestart: true, message: manualRestartMessage, }; } return { success: true, requiresReload: true, message: liveMessage, reloadDelayMs: clientReloadDelayMs, }; }; const completeMcpMutation = async (res, action, name, applyChange) => { applyChange(); try { await refreshOpenCodeAfterConfigChange(`mcp ${action}`); return res.json({ success: true, requiresReload: true, message: `MCP server "${name}" ${action}d. Reloading interface…`, reloadDelayMs: clientReloadDelayMs, }); } catch (error) { console.error(`[API:MCP ${action}] Reload failed after config write:`, error); return res.json({ success: true, requiresReload: false, reloadFailed: true, message: `MCP server "${name}" ${action}d, but OpenCode reload failed.`, warning: error.message || 'OpenCode reload failed after the MCP configuration changed', }); } }; app.get('/api/config/agents/:name', async (req, res) => { try { const agentName = req.params.name; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } const sources = getAgentSources(agentName, directory); const scope = sources.md.exists ? sources.md.scope : (sources.json.exists ? sources.json.scope : null); res.json({ name: agentName, sources: sources, scope, isBuiltIn: !sources.md.exists && !sources.json.exists }); } catch (error) { console.error('Failed to get agent sources:', error); res.status(500).json({ error: 'Failed to get agent configuration metadata' }); } }); app.get('/api/config/agents/:name/config', async (req, res) => { try { const agentName = req.params.name; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } const configInfo = getAgentConfig(agentName, directory); res.json(configInfo); } catch (error) { console.error('Failed to get agent config:', error); res.status(500).json({ error: 'Failed to get agent configuration' }); } }); app.post('/api/config/agents/:name', async (req, res) => { try { const agentName = req.params.name; const { scope, ...config } = req.body; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } console.log('[Server] Creating agent:', agentName); console.log('[Server] Config received:', JSON.stringify(config, null, 2)); console.log('[Server] Scope:', scope, 'Working directory:', directory); createAgent(agentName, config, directory, scope); const refreshResult = await refreshOpenCodeAfterConfigChange('agent creation', { agentName }); res.json(buildConfigMutationResponse(refreshResult, { liveMessage: `Agent ${agentName} created successfully. Reloading interface…`, manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`, })); } catch (error) { console.error('Failed to create agent:', error); res.status(500).json({ error: error.message || 'Failed to create agent' }); } }); app.patch('/api/config/agents/:name', async (req, res) => { try { const agentName = req.params.name; const updates = req.body; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } console.log(`[Server] Updating agent: ${agentName}`); console.log('[Server] Updates:', JSON.stringify(updates, null, 2)); console.log('[Server] Working directory:', directory); updateAgent(agentName, updates, directory); const refreshResult = await refreshOpenCodeAfterConfigChange('agent update'); console.log(`[Server] Agent ${agentName} updated successfully`); res.json(buildConfigMutationResponse(refreshResult, { liveMessage: `Agent ${agentName} updated successfully. Reloading interface…`, manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`, })); } catch (error) { console.error('[Server] Failed to update agent:', error); console.error('[Server] Error stack:', error.stack); res.status(500).json({ error: error.message || 'Failed to update agent' }); } }); app.delete('/api/config/agents/:name', async (req, res) => { try { const agentName = req.params.name; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } const scope = req.body?.scope; deleteAgent(agentName, directory, scope); const refreshResult = await refreshOpenCodeAfterConfigChange('agent deletion'); res.json(buildConfigMutationResponse(refreshResult, { liveMessage: `Agent ${agentName} deleted successfully. Reloading interface…`, manualRestartMessage: `Agent ${agentName} deleted. Restart your connected OpenCode server to apply the change.`, })); } catch (error) { console.error('Failed to delete agent:', error); res.status(500).json({ error: error.message || 'Failed to delete agent' }); } }); app.get('/api/config/mcp', async (req, res) => { try { const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } const configs = listMcpConfigs(directory); res.json(configs); } catch (error) { console.error('[API:GET /api/config/mcp] Failed:', error); res.status(500).json({ error: error.message || 'Failed to list MCP configs' }); } }); app.get('/api/config/mcp/:name', async (req, res) => { try { const name = req.params.name; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } const config = getMcpConfig(name, directory); if (!config) { return res.status(404).json({ error: `MCP server "${name}" not found` }); } res.json(config); } catch (error) { console.error('[API:GET /api/config/mcp/:name] Failed:', error); res.status(500).json({ error: error.message || 'Failed to get MCP config' }); } }); app.post('/api/config/mcp/:name', async (req, res) => { try { const name = req.params.name; const { scope, ...config } = req.body || {}; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } console.log(`[API:POST /api/config/mcp] Creating MCP server: ${name}`); await completeMcpMutation(res, 'create', name, () => { createMcpConfig(name, config, directory, scope); }); } catch (error) { console.error('[API:POST /api/config/mcp/:name] Failed:', error); res.status(500).json({ error: error.message || 'Failed to create MCP server' }); } }); app.patch('/api/config/mcp/:name', async (req, res) => { try { const name = req.params.name; const updates = req.body; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } console.log(`[API:PATCH /api/config/mcp] Updating MCP server: ${name}`); await completeMcpMutation(res, 'update', name, () => { updateMcpConfig(name, updates, directory); }); } catch (error) { console.error('[API:PATCH /api/config/mcp/:name] Failed:', error); if (error?.message === `MCP server "${req.params.name}" not found`) { return res.status(404).json({ error: error.message }); } res.status(500).json({ error: error.message || 'Failed to update MCP server' }); } }); app.delete('/api/config/mcp/:name', async (req, res) => { try { const name = req.params.name; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } console.log(`[API:DELETE /api/config/mcp] Deleting MCP server: ${name}`); await completeMcpMutation(res, 'delete', name, () => { deleteMcpConfig(name, directory); }); } catch (error) { console.error('[API:DELETE /api/config/mcp/:name] Failed:', error); res.status(500).json({ error: error.message || 'Failed to delete MCP server' }); } }); app.get('/api/config/commands/:name', async (req, res) => { try { const commandName = req.params.name; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } const sources = getCommandSources(commandName, directory); const scope = sources.md.exists ? sources.md.scope : (sources.json.exists ? sources.json.scope : null); res.json({ name: commandName, sources: sources, scope, isBuiltIn: !sources.md.exists && !sources.json.exists }); } catch (error) { console.error('Failed to get command sources:', error); res.status(500).json({ error: 'Failed to get command configuration metadata' }); } }); app.post('/api/config/commands/:name', async (req, res) => { try { const commandName = req.params.name; const { scope, ...config } = req.body; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } console.log('[Server] Creating command:', commandName); console.log('[Server] Config received:', JSON.stringify(config, null, 2)); console.log('[Server] Scope:', scope, 'Working directory:', directory); createCommand(commandName, config, directory, scope); await refreshOpenCodeAfterConfigChange('command creation', { commandName }); res.json({ success: true, requiresReload: true, message: `Command ${commandName} created successfully. Reloading interface…`, reloadDelayMs: clientReloadDelayMs, }); } catch (error) { console.error('Failed to create command:', error); res.status(500).json({ error: error.message || 'Failed to create command' }); } }); app.patch('/api/config/commands/:name', async (req, res) => { try { const commandName = req.params.name; const updates = req.body; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } console.log(`[Server] Updating command: ${commandName}`); console.log('[Server] Updates:', JSON.stringify(updates, null, 2)); console.log('[Server] Working directory:', directory); updateCommand(commandName, updates, directory); await refreshOpenCodeAfterConfigChange('command update'); console.log(`[Server] Command ${commandName} updated successfully`); res.json({ success: true, requiresReload: true, message: `Command ${commandName} updated successfully. Reloading interface…`, reloadDelayMs: clientReloadDelayMs, }); } catch (error) { console.error('[Server] Failed to update command:', error); console.error('[Server] Error stack:', error.stack); res.status(500).json({ error: error.message || 'Failed to update command' }); } }); app.delete('/api/config/commands/:name', async (req, res) => { try { const commandName = req.params.name; const { directory, error } = await resolveProjectDirectory(req); if (!directory) { return res.status(400).json({ error }); } deleteCommand(commandName, directory); await refreshOpenCodeAfterConfigChange('command deletion'); res.json({ success: true, requiresReload: true, message: `Command ${commandName} deleted successfully. Reloading interface…`, reloadDelayMs: clientReloadDelayMs, }); } catch (error) { console.error('Failed to delete command:', error); res.status(500).json({ error: error.message || 'Failed to delete command' }); } }); app.get('/api/config/snippets', async (req, res) => { try { const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } res.json(listSnippets(directory)); } catch (error) { console.error('[API:GET /api/config/snippets] Failed:', error); res.status(500).json({ error: error.message || 'Failed to list snippets' }); } }); app.post('/api/config/snippets/expand', async (req, res) => { try { const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } res.json({ text: expandSnippets(req.body?.text ?? '', directory) }); } catch (error) { console.error('[API:POST /api/config/snippets/expand] Failed:', error); res.status(500).json({ error: error.message || 'Failed to expand snippets' }); } }); app.get('/api/config/snippets/:name', async (req, res) => { try { const name = req.params.name; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } const snippet = getSnippet(name, directory); if (!snippet) { return res.status(404).json({ error: `Snippet "${name}" not found` }); } res.json(snippet); } catch (error) { console.error('[API:GET /api/config/snippets/:name] Failed:', error); if (error.message?.includes('Snippet name')) { return res.status(400).json({ error: error.message }); } res.status(500).json({ error: error.message || 'Failed to get snippet' }); } }); app.post('/api/config/snippets/:name', async (req, res) => { try { const name = req.params.name; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } const snippet = createSnippet(name, req.body || {}, directory, req.body?.scope || 'global'); res.json({ success: true, snippet }); } catch (error) { console.error('[API:POST /api/config/snippets/:name] Failed:', error); if (error.message?.includes('already exists')) { return res.status(409).json({ error: error.message }); } if (error.message?.includes('Snippet name') || error.message?.includes('Project directory')) { return res.status(400).json({ error: error.message }); } res.status(500).json({ error: error.message || 'Failed to create snippet' }); } }); app.patch('/api/config/snippets/:name', async (req, res) => { try { const name = req.params.name; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } res.json({ success: true, snippet: updateSnippet(name, req.body || {}, directory) }); } catch (error) { console.error('[API:PATCH /api/config/snippets/:name] Failed:', error); if (error.message?.includes('not found')) { return res.status(404).json({ error: error.message }); } if (error.message?.includes('Snippet name')) { return res.status(400).json({ error: error.message }); } res.status(500).json({ error: error.message || 'Failed to update snippet' }); } }); app.delete('/api/config/snippets/:name', async (req, res) => { try { const name = req.params.name; const { directory, error } = await resolveOptionalProjectDirectory(req); if (error) { return res.status(400).json({ error }); } deleteSnippet(name, directory); res.json({ success: true }); } catch (error) { console.error('[API:DELETE /api/config/snippets/:name] Failed:', error); if (error.message?.includes('not found')) { return res.status(404).json({ error: error.message }); } if (error.message?.includes('Snippet name')) { return res.status(400).json({ error: error.message }); } res.status(500).json({ error: error.message || 'Failed to delete snippet' }); } }); };