From a93be506a3ab6f3583010073b426fe5b4cbfd23b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Mar 2026 19:28:48 +0000 Subject: [PATCH 1/2] fix(config): tolerate trailing commas in micode.json and warn on parse errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-edited JSON configs commonly include trailing commas, which causes JSON.parse to throw. The catch block silently returns null, so all user overrides are lost with zero feedback. - Add stripTrailingCommas helper using a string-safe regex that skips quoted values (prevents corrupting strings like "hello, }") - Apply to all three JSON.parse sites (opencode.json, micode.json, model context limits) - Distinguish file-not-found (silent) from parse errors in loadMicodeConfig — log a clear warning via log.warn so users know their config was ignored https://claude.ai/code/session_019WCTQeKgwFg1RG2rrBDjas --- src/config-loader.ts | 29 ++++++++++++++++--- tests/config-loader.test.ts | 55 +++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/config-loader.ts b/src/config-loader.ts index 6129782..2dcae09 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -6,6 +6,20 @@ import { join } from "node:path"; import type { AgentConfig } from "@opencode-ai/sdk"; +import { log } from "./utils/logger"; + +/** + * Strip trailing commas from JSON strings to be lenient with hand-edited configs. + * Matches quoted strings first (and keeps them intact) so that commas inside + * string values like "hello, }" are never modified. + */ +function stripTrailingCommas(json: string): string { + return json.replace(/"(?:[^"\\]|\\.)*"|,\s*([\]}])/g, (match, bracket) => { + if (bracket === undefined) return match; + return bracket; + }); +} + // Minimal type for provider validation - only what we need export interface ProviderInfo { id: string; @@ -30,7 +44,7 @@ function loadOpencodeConfig(configDir?: string): OpencodeConfig | null { try { const configPath = join(baseDir, "opencode.json"); const content = readFileSync(configPath, "utf-8"); - return JSON.parse(content) as OpencodeConfig; + return JSON.parse(stripTrailingCommas(content)) as OpencodeConfig; } catch { return null; } @@ -100,7 +114,7 @@ export async function loadMicodeConfig(configDir?: string): Promise; + const parsed = JSON.parse(stripTrailingCommas(content)) as Record; const result: MicodeConfig = {}; @@ -160,7 +174,14 @@ export async function loadMicodeConfig(configDir?: string): Promise try { const configPath = join(baseDir, "opencode.json"); const content = readFileSync(configPath, "utf-8"); - const config = JSON.parse(content) as { + const config = JSON.parse(stripTrailingCommas(content)) as { provider?: Record }>; }; diff --git a/tests/config-loader.test.ts b/tests/config-loader.test.ts index f803cd7..8160f8b 100644 --- a/tests/config-loader.test.ts +++ b/tests/config-loader.test.ts @@ -1,5 +1,5 @@ // tests/config-loader.test.ts -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -44,12 +44,61 @@ describe("config-loader", () => { expect(config?.agents?.brainstormer?.temperature).toBe(0.5); }); - it("should return null for invalid JSON", async () => { + it("should return null and warn for invalid JSON", async () => { const configPath = join(testConfigDir, "micode.json"); - writeFileSync(configPath, "{ invalid json }"); + writeFileSync(configPath, '{ "agents": BROKEN }'); + + const warnSpy = spyOn(console, "warn").mockImplementation(() => {}); const config = await loadMicodeConfig(testConfigDir); + expect(config).toBeNull(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to parse micode.json")); + + warnSpy.mockRestore(); + }); + + it("should parse JSON with trailing commas", async () => { + const configPath = join(testConfigDir, "micode.json"); + writeFileSync( + configPath, + `{ + "agents": { + "commander": { "model": "openai/gpt-4o", }, + "brainstormer": { "model": "anthropic/claude-opus-4-6", "temperature": 0.8, }, + }, + "compactionThreshold": 0.5, + }`, + ); + + const config = await loadMicodeConfig(testConfigDir); + + expect(config).not.toBeNull(); + expect(config?.agents?.commander?.model).toBe("openai/gpt-4o"); + expect(config?.agents?.brainstormer?.model).toBe("anthropic/claude-opus-4-6"); + expect(config?.agents?.brainstormer?.temperature).toBe(0.8); + expect(config?.compactionThreshold).toBe(0.5); + }); + + it("should not corrupt string values containing commas and brackets", async () => { + const configPath = join(testConfigDir, "micode.json"); + writeFileSync( + configPath, + JSON.stringify({ + fragments: { + brainstormer: ["hello, }", "keep this, ]too", 'escaped \\" comma, }'], + }, + }), + ); + + const config = await loadMicodeConfig(testConfigDir); + + expect(config).not.toBeNull(); + expect(config?.fragments?.brainstormer).toEqual([ + "hello, }", + "keep this, ]too", + 'escaped \\" comma, }', + ]); }); it("should handle empty agents object", async () => { From ef676ba9c37202ec9243b8ecd4110d5c8e57b759 Mon Sep 17 00:00:00 2001 From: Bruno Alves Date: Mon, 9 Mar 2026 16:51:32 -0300 Subject: [PATCH 2/2] fix: distinguish read errors from parse errors in loadMicodeConfig Split the single try/catch in loadMicodeConfig into two separate try/catch blocks: one for the readFile call and one for JSON.parse and validation. Filesystem errors (permissions, I/O, etc.) now log "Failed to read micode.json" while JSON syntax errors continue to log "Failed to parse micode.json", so users get accurate guidance on what actually went wrong. --- src/config-loader.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/config-loader.ts b/src/config-loader.ts index 2dcae09..4de393f 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -112,8 +112,20 @@ export async function loadMicodeConfig(configDir?: string): Promise; const result: MicodeConfig = {}; @@ -175,10 +187,6 @@ export async function loadMicodeConfig(configDir?: string): Promise