fix(config): deep merge agent overrides with reusable deepMerge utility (#27)
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
12
src/index.ts
12
src/index.ts
@@ -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
53
src/shared/deep-merge.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user