Skip to content

Commit 3b129f1

Browse files
authored
fix(config): deep merge agent overrides with reusable deepMerge utility (#27)
1 parent 2cab36f commit 3b129f1

File tree

4 files changed

+59
-19
lines changed

4 files changed

+59
-19
lines changed

src/agents/utils.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { librarianAgent } from "./librarian"
55
import { exploreAgent } from "./explore"
66
import { frontendUiUxEngineerAgent } from "./frontend-ui-ux-engineer"
77
import { documentWriterAgent } from "./document-writer"
8+
import { deepMerge } from "../shared"
89

910
const allBuiltinAgents: Record<AgentName, AgentConfig> = {
1011
oracle: oracleAgent,
@@ -18,16 +19,7 @@ function mergeAgentConfig(
1819
base: AgentConfig,
1920
override: AgentOverrideConfig
2021
): AgentConfig {
21-
return {
22-
...base,
23-
...override,
24-
tools: override.tools !== undefined
25-
? { ...(base.tools ?? {}), ...override.tools }
26-
: base.tools,
27-
permission: override.permission !== undefined
28-
? { ...(base.permission ?? {}), ...override.permission }
29-
: base.permission,
30-
}
22+
return deepMerge(base, override as Partial<AgentConfig>)
3123
}
3224

3325
export function createBuiltinAgents(

src/index.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools
4242
import { BackgroundManager } from "./features/background-agent";
4343
import { createBuiltinMcps } from "./mcp";
4444
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
45-
import { log } from "./shared/logger";
45+
import { log, deepMerge } from "./shared";
4646
import * as fs from "fs";
4747
import * as path from "path";
4848
import * as os from "os";
@@ -89,10 +89,7 @@ function mergeConfigs(
8989
return {
9090
...base,
9191
...override,
92-
agents:
93-
override.agents !== undefined
94-
? { ...(base.agents ?? {}), ...override.agents }
95-
: base.agents,
92+
agents: deepMerge(base.agents, override.agents),
9693
disabled_agents: [
9794
...new Set([
9895
...(base.disabled_agents ?? []),
@@ -105,10 +102,7 @@ function mergeConfigs(
105102
...(override.disabled_mcps ?? []),
106103
]),
107104
],
108-
claude_code:
109-
override.claude_code !== undefined || base.claude_code !== undefined
110-
? { ...(base.claude_code ?? {}), ...(override.claude_code ?? {}) }
111-
: undefined,
105+
claude_code: deepMerge(base.claude_code, override.claude_code),
112106
};
113107
}
114108

src/shared/deep-merge.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
2+
const MAX_DEPTH = 50;
3+
4+
function isPlainObject(value: unknown): value is Record<string, unknown> {
5+
return (
6+
typeof value === "object" &&
7+
value !== null &&
8+
!Array.isArray(value) &&
9+
Object.prototype.toString.call(value) === "[object Object]"
10+
);
11+
}
12+
13+
/**
14+
* Deep merges two objects, with override values taking precedence.
15+
* - Objects are recursively merged
16+
* - Arrays are replaced (not concatenated)
17+
* - undefined values in override do not overwrite base values
18+
*
19+
* @example
20+
* deepMerge({ a: 1, b: { c: 2, d: 3 } }, { b: { c: 10 }, e: 5 })
21+
* // => { a: 1, b: { c: 10, d: 3 }, e: 5 }
22+
*/
23+
export function deepMerge<T extends Record<string, unknown>>(base: T, override: Partial<T>, depth?: number): T;
24+
export function deepMerge<T extends Record<string, unknown>>(base: T | undefined, override: T | undefined, depth?: number): T | undefined;
25+
export function deepMerge<T extends Record<string, unknown>>(
26+
base: T | undefined,
27+
override: T | undefined,
28+
depth = 0
29+
): T | undefined {
30+
if (!base && !override) return undefined;
31+
if (!base) return override;
32+
if (!override) return base;
33+
if (depth > MAX_DEPTH) return override ?? base;
34+
35+
const result = { ...base } as Record<string, unknown>;
36+
37+
for (const key of Object.keys(override)) {
38+
if (DANGEROUS_KEYS.has(key)) continue;
39+
40+
const baseValue = base[key];
41+
const overrideValue = override[key];
42+
43+
if (overrideValue === undefined) continue;
44+
45+
if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
46+
result[key] = deepMerge(baseValue, overrideValue, depth + 1);
47+
} else {
48+
result[key] = overrideValue;
49+
}
50+
}
51+
52+
return result as T;
53+
}

src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from "./snake-case"
77
export * from "./tool-name"
88
export * from "./pattern-matcher"
99
export * from "./hook-disabled"
10+
export * from "./deep-merge"

0 commit comments

Comments
 (0)