fix(config): deep merge agent overrides with reusable deepMerge utility (#27)

This commit is contained in:
Junho Yeo
2025-12-13 12:00:38 +09:00
committed by GitHub
parent 2cab36f06d
commit 3b129f11c4
4 changed files with 59 additions and 19 deletions

View File

@@ -5,6 +5,7 @@ import { librarianAgent } from "./librarian"
import { exploreAgent } from "./explore" import { exploreAgent } from "./explore"
import { frontendUiUxEngineerAgent } from "./frontend-ui-ux-engineer" import { frontendUiUxEngineerAgent } from "./frontend-ui-ux-engineer"
import { documentWriterAgent } from "./document-writer" import { documentWriterAgent } from "./document-writer"
import { deepMerge } from "../shared"
const allBuiltinAgents: Record<AgentName, AgentConfig> = { const allBuiltinAgents: Record<AgentName, AgentConfig> = {
oracle: oracleAgent, oracle: oracleAgent,
@@ -18,16 +19,7 @@ function mergeAgentConfig(
base: AgentConfig, base: AgentConfig,
override: AgentOverrideConfig override: AgentOverrideConfig
): AgentConfig { ): AgentConfig {
return { return deepMerge(base, override as Partial<AgentConfig>)
...base,
...override,
tools: override.tools !== undefined
? { ...(base.tools ?? {}), ...override.tools }
: base.tools,
permission: override.permission !== undefined
? { ...(base.permission ?? {}), ...override.permission }
: base.permission,
}
} }
export function createBuiltinAgents( export function createBuiltinAgents(

View File

@@ -42,7 +42,7 @@ import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools
import { BackgroundManager } from "./features/background-agent"; import { BackgroundManager } from "./features/background-agent";
import { createBuiltinMcps } from "./mcp"; import { createBuiltinMcps } from "./mcp";
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config"; import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
import { log } from "./shared/logger"; import { log, deepMerge } from "./shared";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import * as os from "os"; import * as os from "os";
@@ -89,10 +89,7 @@ function mergeConfigs(
return { return {
...base, ...base,
...override, ...override,
agents: agents: deepMerge(base.agents, override.agents),
override.agents !== undefined
? { ...(base.agents ?? {}), ...override.agents }
: base.agents,
disabled_agents: [ disabled_agents: [
...new Set([ ...new Set([
...(base.disabled_agents ?? []), ...(base.disabled_agents ?? []),
@@ -105,10 +102,7 @@ function mergeConfigs(
...(override.disabled_mcps ?? []), ...(override.disabled_mcps ?? []),
]), ]),
], ],
claude_code: claude_code: deepMerge(base.claude_code, override.claude_code),
override.claude_code !== undefined || base.claude_code !== undefined
? { ...(base.claude_code ?? {}), ...(override.claude_code ?? {}) }
: undefined,
}; };
} }

53
src/shared/deep-merge.ts Normal file
View File

@@ -0,0 +1,53 @@
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
const MAX_DEPTH = 50;
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
/**
* Deep merges two objects, with override values taking precedence.
* - Objects are recursively merged
* - Arrays are replaced (not concatenated)
* - undefined values in override do not overwrite base values
*
* @example
* deepMerge({ a: 1, b: { c: 2, d: 3 } }, { b: { c: 10 }, e: 5 })
* // => { a: 1, b: { c: 10, d: 3 }, e: 5 }
*/
export function deepMerge<T extends Record<string, unknown>>(base: T, override: Partial<T>, depth?: number): T;
export function deepMerge<T extends Record<string, unknown>>(base: T | undefined, override: T | undefined, depth?: number): T | undefined;
export function deepMerge<T extends Record<string, unknown>>(
base: T | undefined,
override: T | undefined,
depth = 0
): T | undefined {
if (!base && !override) return undefined;
if (!base) return override;
if (!override) return base;
if (depth > MAX_DEPTH) return override ?? base;
const result = { ...base } as Record<string, unknown>;
for (const key of Object.keys(override)) {
if (DANGEROUS_KEYS.has(key)) continue;
const baseValue = base[key];
const overrideValue = override[key];
if (overrideValue === undefined) continue;
if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
result[key] = deepMerge(baseValue, overrideValue, depth + 1);
} else {
result[key] = overrideValue;
}
}
return result as T;
}

View File

@@ -7,3 +7,4 @@ export * from "./snake-case"
export * from "./tool-name" export * from "./tool-name"
export * from "./pattern-matcher" export * from "./pattern-matcher"
export * from "./hook-disabled" export * from "./hook-disabled"
export * from "./deep-merge"