diff --git a/src/config/includes-scan.ts b/src/config/includes-scan.ts index 7542ecd8bc7..16e42e1c04e 100644 --- a/src/config/includes-scan.ts +++ b/src/config/includes-scan.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs/promises"; import path from "node:path"; -import JSON5 from "json5"; +import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js"; import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "./includes.js"; function listDirectIncludes(parsed: unknown): string[] { @@ -70,7 +70,7 @@ export async function collectIncludePathsRecursive(params: { } const nestedParsed = (() => { try { - return JSON5.parse(rawText); + return parseJsonWithJson5Fallback(rawText); } catch { return null; } diff --git a/src/config/includes.ts b/src/config/includes.ts index 7716987933a..e134973b215 100644 --- a/src/config/includes.ts +++ b/src/config/includes.ts @@ -12,10 +12,10 @@ import fs from "node:fs"; import path from "node:path"; -import JSON5 from "json5"; import { canUseRootFileOpen, openRootFileSync } from "../infra/boundary-file-read.js"; import { isPathInside } from "../security/scan-paths.js"; import { isPlainObject } from "../utils.js"; +import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js"; import { isBlockedObjectKey } from "./prototype-keys.js"; export const INCLUDE_KEY = "$include"; @@ -421,7 +421,7 @@ const defaultResolver: IncludeResolver = { readFile: (p) => fs.readFileSync(p, "utf-8"), readFileWithGuards: ({ includePath, resolvedPath, rootRealDir }) => readConfigIncludeFileWithGuards({ includePath, resolvedPath, rootRealDir }), - parseJson: (raw) => JSON5.parse(raw), + parseJson: parseJsonWithJson5Fallback, }; /** diff --git a/src/config/io.parse.test.ts b/src/config/io.parse.test.ts new file mode 100644 index 00000000000..b770d7fd6ed --- /dev/null +++ b/src/config/io.parse.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it, vi } from "vitest"; +import { parseConfigJson5 } from "./config.js"; + +describe("parseConfigJson5", () => { + it("uses native JSON parsing before JSON5 fallback", () => { + const json5 = { parse: vi.fn(() => ({ fromJson5: true })) }; + + const result = parseConfigJson5('{"gateway":{"mode":"local"}}', json5); + + expect(result).toEqual({ ok: true, parsed: { gateway: { mode: "local" } } }); + expect(json5.parse).not.toHaveBeenCalled(); + }); + + it("falls back to JSON5 for authored config syntax", () => { + const json5 = { parse: vi.fn(() => ({ gateway: { mode: "local" } })) }; + + const result = parseConfigJson5("{ gateway: { mode: 'local' } }", json5); + + expect(result).toEqual({ ok: true, parsed: { gateway: { mode: "local" } } }); + expect(json5.parse).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/config/io.ts b/src/config/io.ts index 282c0d809ae..15676049c2f 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -1042,6 +1042,12 @@ export function parseConfigJson5( raw: string, json5: { parse: (value: string) => unknown } = JSON5, ): ParseConfigJson5Result { + try { + return { ok: true, parsed: JSON.parse(raw) }; + } catch { + // Keep JSON5 compatibility for authored config, but avoid the slower parser + // on the JSON files OpenClaw writes itself. + } try { return { ok: true, parsed: json5.parse(raw) }; } catch (err) {