From 6c10edf8ba0553498649e4e646307578f900a0fc Mon Sep 17 00:00:00 2001 From: Zireael <3856578+Zireael@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:34:23 +0700 Subject: [PATCH] fix(cli-tools): tolerate JSONC configs in CLI tool settings routes readConfig/readSettings/readJson across all CLI tool settings routes used JSON.parse() but only caught ENOENT, re-throwing SyntaxError on any corrupted or JSONC-formatted config file. The API would return 500, which the UI misinterpreted as "tool not installed." This patch: - strips trailing commas before parsing (handles JSONC from opencode, etc.) - returns null on any parse error instead of re-throwing, so the UI shows "installed but not configured" (accurate) instead of "not installed" - applies the same fix to all 8 affected routes: opencode, openclaw, kilo, droid, cowork, copilot, claude, and cline Co-authored-by: Cursor --- .../api/cli-tools/claude-settings/route.js | 10 ++++---- src/app/api/cli-tools/cline-settings/route.js | 8 ++++--- .../api/cli-tools/copilot-settings/route.js | 8 ++++--- .../api/cli-tools/cowork-settings/route.js | 24 ++++++++++++------- src/app/api/cli-tools/droid-settings/route.js | 8 ++++--- src/app/api/cli-tools/kilo-settings/route.js | 8 ++++--- .../api/cli-tools/openclaw-settings/route.js | 8 ++++--- .../api/cli-tools/opencode-settings/route.js | 10 ++++++-- 8 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/app/api/cli-tools/claude-settings/route.js b/src/app/api/cli-tools/claude-settings/route.js index 121879da..bb5fa06f 100644 --- a/src/app/api/cli-tools/claude-settings/route.js +++ b/src/app/api/cli-tools/claude-settings/route.js @@ -41,12 +41,12 @@ const readSettings = async () => { try { const settingsPath = getClaudeSettingsPath(); const content = await fs.readFile(settingsPath, "utf-8"); - return JSON.parse(content); + // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); } catch (error) { - if (error.code === "ENOENT") { - return null; - } - throw error; + return null; } }; diff --git a/src/app/api/cli-tools/cline-settings/route.js b/src/app/api/cli-tools/cline-settings/route.js index cc723979..ecbccbd2 100644 --- a/src/app/api/cli-tools/cline-settings/route.js +++ b/src/app/api/cli-tools/cline-settings/route.js @@ -35,10 +35,12 @@ const checkInstalled = async () => { const readJson = async (filePath) => { try { const content = await fs.readFile(filePath, "utf-8"); - return JSON.parse(content); + // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); } catch (error) { - if (error.code === "ENOENT") return null; - throw error; + return null; } }; diff --git a/src/app/api/cli-tools/copilot-settings/route.js b/src/app/api/cli-tools/copilot-settings/route.js index 449c6e2c..3c0bd669 100644 --- a/src/app/api/cli-tools/copilot-settings/route.js +++ b/src/app/api/cli-tools/copilot-settings/route.js @@ -21,10 +21,12 @@ const getConfigPath = () => { const readConfig = async () => { try { const content = await fs.readFile(getConfigPath(), "utf-8"); - return JSON.parse(content); + // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); } catch (error) { - if (error.code === "ENOENT") return null; - throw error; + return null; } }; diff --git a/src/app/api/cli-tools/cowork-settings/route.js b/src/app/api/cli-tools/cowork-settings/route.js index e09ec283..d30b667e 100644 --- a/src/app/api/cli-tools/cowork-settings/route.js +++ b/src/app/api/cli-tools/cowork-settings/route.js @@ -116,10 +116,14 @@ const get1pRoot = () => { const get1pConfigPath = () => path.join(get1pRoot(), "claude_desktop_config.json"); const read1pConfig = async () => { - try { return JSON.parse(await fs.readFile(get1pConfigPath(), "utf-8")) || {}; } - catch (error) { - if (error.code === "ENOENT") return {}; - throw error; + try { + const content = await fs.readFile(get1pConfigPath(), "utf-8"); + // Tolerate JSONC (trailing commas) and treat unparseable files as empty config + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped) || {}; + } catch (error) { + return {}; } }; @@ -193,10 +197,14 @@ const checkInstalled = async () => { }; const readJson = async (filePath) => { - try { return JSON.parse(await fs.readFile(filePath, "utf-8")); } - catch (error) { - if (error.code === "ENOENT") return null; - throw error; + try { + const content = await fs.readFile(filePath, "utf-8"); + // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); + } catch (error) { + return null; } }; diff --git a/src/app/api/cli-tools/droid-settings/route.js b/src/app/api/cli-tools/droid-settings/route.js index 34f53262..a4162578 100644 --- a/src/app/api/cli-tools/droid-settings/route.js +++ b/src/app/api/cli-tools/droid-settings/route.js @@ -37,10 +37,12 @@ const readSettings = async () => { try { const settingsPath = getDroidSettingsPath(); const content = await fs.readFile(settingsPath, "utf-8"); - return JSON.parse(content); + // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); } catch (error) { - if (error.code === "ENOENT") return null; - throw error; + return null; } }; diff --git a/src/app/api/cli-tools/kilo-settings/route.js b/src/app/api/cli-tools/kilo-settings/route.js index 6802adc8..9c5993f2 100644 --- a/src/app/api/cli-tools/kilo-settings/route.js +++ b/src/app/api/cli-tools/kilo-settings/route.js @@ -35,10 +35,12 @@ const checkInstalled = async () => { const readJson = async (filePath) => { try { const content = await fs.readFile(filePath, "utf-8"); - return JSON.parse(content); + // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); } catch (error) { - if (error.code === "ENOENT") return null; - throw error; + return null; } }; diff --git a/src/app/api/cli-tools/openclaw-settings/route.js b/src/app/api/cli-tools/openclaw-settings/route.js index 85af6a92..2047a256 100644 --- a/src/app/api/cli-tools/openclaw-settings/route.js +++ b/src/app/api/cli-tools/openclaw-settings/route.js @@ -47,10 +47,12 @@ const readSettings = async () => { try { const settingsPath = getOpenClawSettingsPath(); const content = await fs.readFile(settingsPath, "utf-8"); - return JSON.parse(content); + // Tolerate JSONC (trailing commas) and treat unparseable files as "no config" + // rather than throwing a 500 that the UI misreads as "tool not installed". + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); } catch (error) { - if (error.code === "ENOENT") return null; - throw error; + return null; } }; diff --git a/src/app/api/cli-tools/opencode-settings/route.js b/src/app/api/cli-tools/opencode-settings/route.js index f4429cf5..03819c66 100644 --- a/src/app/api/cli-tools/opencode-settings/route.js +++ b/src/app/api/cli-tools/opencode-settings/route.js @@ -35,10 +35,16 @@ const checkOpenCodeInstalled = async () => { const readConfig = async () => { try { const content = await fs.readFile(getConfigPath(), "utf-8"); - return JSON.parse(content); + // opencode config files may use JSONC format (trailing commas, comments). + // Strip trailing commas before parsing to avoid SyntaxError on valid JSONC. + const stripped = content.replace(/,(\s*[}\]])/g, "$1"); + return JSON.parse(stripped); } catch (error) { if (error.code === "ENOENT") return null; - throw error; + // If the config file exists but is unparseable (corrupted, exotic JSONC), + // treat it as "no config" rather than throwing a 500 that the UI + // misinterprets as "opencode not installed". + return null; } };