fix: add oh-my-openagent.jsonc config file detection (fixes #2624)

This commit is contained in:
YeonGyu-Kim
2026-03-23 18:11:01 +09:00
parent d886ac701f
commit 7f20dd6ff5
7 changed files with 256 additions and 25 deletions

View File

@@ -3736,6 +3736,147 @@
},
"additionalProperties": false
},
"openclaw": {
"type": "object",
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"gateways": {
"default": {},
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"type": {
"default": "http",
"type": "string",
"enum": [
"http",
"command"
]
},
"url": {
"type": "string"
},
"method": {
"default": "POST",
"type": "string"
},
"headers": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"command": {
"type": "string"
},
"timeout": {
"type": "number"
}
},
"required": [
"type",
"method"
],
"additionalProperties": false
}
},
"hooks": {
"default": {},
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"enabled": {
"default": true,
"type": "boolean"
},
"gateway": {
"type": "string"
},
"instruction": {
"type": "string"
}
},
"required": [
"enabled",
"gateway",
"instruction"
],
"additionalProperties": false
}
},
"replyListener": {
"type": "object",
"properties": {
"discordBotToken": {
"type": "string"
},
"discordChannelId": {
"type": "string"
},
"discordMention": {
"type": "string"
},
"authorizedDiscordUserIds": {
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"telegramBotToken": {
"type": "string"
},
"telegramChatId": {
"type": "string"
},
"pollIntervalMs": {
"default": 3000,
"type": "number"
},
"rateLimitPerMinute": {
"default": 10,
"type": "number"
},
"maxMessageLength": {
"default": 500,
"type": "number"
},
"includePrefix": {
"default": true,
"type": "boolean"
}
},
"required": [
"authorizedDiscordUserIds",
"pollIntervalMs",
"rateLimitPerMinute",
"maxMessageLength",
"includePrefix"
],
"additionalProperties": false
}
},
"required": [
"enabled",
"gateways",
"hooks"
],
"additionalProperties": false
},
"babysitting": {
"type": "object",
"properties": {

View File

@@ -2,15 +2,15 @@ import { readFileSync } from "node:fs"
import { join } from "node:path"
import { OhMyOpenCodeConfigSchema } from "../../../config"
import { detectConfigFile, getOpenCodeConfigDir, parseJsonc } from "../../../shared"
import { detectPluginConfigFile, getOpenCodeConfigDir, parseJsonc } from "../../../shared"
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
import type { CheckResult, DoctorIssue } from "../types"
import { loadAvailableModelsFromCache } from "./model-resolution-cache"
import { getModelResolutionInfoWithOverrides } from "./model-resolution"
import type { OmoConfig } from "./model-resolution-types"
const USER_CONFIG_BASE = join(getOpenCodeConfigDir({ binary: "opencode" }), PACKAGE_NAME)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
interface ConfigValidationResult {
exists: boolean
@@ -21,10 +21,10 @@ interface ConfigValidationResult {
}
function findConfigPath(): string | null {
const projectConfig = detectConfigFile(PROJECT_CONFIG_BASE)
const projectConfig = detectPluginConfigFile(PROJECT_CONFIG_DIR)
if (projectConfig.format !== "none") return projectConfig.path
const userConfig = detectConfigFile(USER_CONFIG_BASE)
const userConfig = detectPluginConfigFile(USER_CONFIG_DIR)
if (userConfig.format !== "none") return userConfig.path
return null

View File

@@ -1,17 +1,13 @@
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { detectConfigFile, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
import { detectPluginConfigFile, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
import type { OmoConfig } from "./model-resolution-types"
const PACKAGE_NAME = "oh-my-opencode"
const USER_CONFIG_BASE = join(
getOpenCodeConfigPaths({ binary: "opencode", version: null }).configDir,
PACKAGE_NAME
)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
const USER_CONFIG_DIR = getOpenCodeConfigPaths({ binary: "opencode", version: null }).configDir
const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
export function loadOmoConfig(): OmoConfig | null {
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
const projectDetected = detectPluginConfigFile(PROJECT_CONFIG_DIR)
if (projectDetected.format !== "none") {
try {
const content = readFileSync(projectDetected.path, "utf-8")
@@ -21,7 +17,7 @@ export function loadOmoConfig(): OmoConfig | null {
}
}
const userDetected = detectConfigFile(USER_CONFIG_BASE)
const userDetected = detectPluginConfigFile(USER_CONFIG_DIR)
if (userDetected.format !== "none") {
try {
const content = readFileSync(userDetected.path, "utf-8")

View File

@@ -7,7 +7,7 @@ import {
getOpenCodeConfigDir,
addConfigLoadError,
parseJsonc,
detectConfigFile,
detectPluginConfigFile,
migrateConfigFile,
} from "./shared";
@@ -162,20 +162,19 @@ export function loadPluginConfig(
): OhMyOpenCodeConfig {
// User-level config path - prefer .jsonc over .json
const configDir = getOpenCodeConfigDir({ binary: "opencode" });
const userBasePath = path.join(configDir, "oh-my-opencode");
const userDetected = detectConfigFile(userBasePath);
const userDetected = detectPluginConfigFile(configDir);
const userConfigPath =
userDetected.format !== "none"
? userDetected.path
: userBasePath + ".json";
: path.join(configDir, "oh-my-openagent.json");
// Project-level config path - prefer .jsonc over .json
const projectBasePath = path.join(directory, ".opencode", "oh-my-opencode");
const projectDetected = detectConfigFile(projectBasePath);
const projectBasePath = path.join(directory, ".opencode");
const projectDetected = detectPluginConfigFile(projectBasePath);
const projectConfigPath =
projectDetected.format !== "none"
? projectDetected.path
: projectBasePath + ".json";
: path.join(projectBasePath, "oh-my-openagent.json");
// Load user config first (base)
let config: OhMyOpenCodeConfig =

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { detectConfigFile, parseJsonc, parseJsoncSafe, readJsoncFile } from "./jsonc-parser"
import { detectConfigFile, detectPluginConfigFile, parseJsonc, parseJsoncSafe, readJsoncFile } from "./jsonc-parser"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
@@ -264,3 +264,84 @@ describe("detectConfigFile", () => {
expect(result.format).toBe("none")
})
})
describe("detectPluginConfigFile", () => {
const testDir = join(__dirname, ".test-detect-plugin")
test("prefers oh-my-openagent over oh-my-opencode", () => {
// given
if (!existsSync(testDir)) mkdirSync(testDir, { recursive: true })
writeFileSync(join(testDir, "oh-my-openagent.jsonc"), "{}")
writeFileSync(join(testDir, "oh-my-opencode.jsonc"), "{}")
// when
const result = detectPluginConfigFile(testDir)
// then
expect(result.format).toBe("jsonc")
expect(result.path).toBe(join(testDir, "oh-my-openagent.jsonc"))
rmSync(testDir, { recursive: true, force: true })
})
test("falls back to oh-my-opencode when oh-my-openagent doesn't exist", () => {
// given
if (!existsSync(testDir)) mkdirSync(testDir, { recursive: true })
writeFileSync(join(testDir, "oh-my-opencode.jsonc"), "{}")
// when
const result = detectPluginConfigFile(testDir)
// then
expect(result.format).toBe("jsonc")
expect(result.path).toBe(join(testDir, "oh-my-opencode.jsonc"))
rmSync(testDir, { recursive: true, force: true })
})
test("falls back to oh-my-opencode.json when no jsonc exists", () => {
// given
if (!existsSync(testDir)) mkdirSync(testDir, { recursive: true })
writeFileSync(join(testDir, "oh-my-opencode.json"), "{}")
// when
const result = detectPluginConfigFile(testDir)
// then
expect(result.format).toBe("json")
expect(result.path).toBe(join(testDir, "oh-my-opencode.json"))
rmSync(testDir, { recursive: true, force: true })
})
test("returns none when no config files exist", () => {
// given
const emptyDir = join(testDir, "empty")
if (!existsSync(emptyDir)) mkdirSync(emptyDir, { recursive: true })
// when
const result = detectPluginConfigFile(emptyDir)
// then
expect(result.format).toBe("none")
expect(result.path).toBe(join(emptyDir, "oh-my-openagent.json"))
rmSync(testDir, { recursive: true, force: true })
})
test("prefers oh-my-openagent.json over oh-my-opencode.jsonc", () => {
// given
if (!existsSync(testDir)) mkdirSync(testDir, { recursive: true })
writeFileSync(join(testDir, "oh-my-openagent.json"), "{}")
writeFileSync(join(testDir, "oh-my-opencode.jsonc"), "{}")
// when
const result = detectPluginConfigFile(testDir)
// then
expect(result.format).toBe("json")
expect(result.path).toBe(join(testDir, "oh-my-openagent.json"))
rmSync(testDir, { recursive: true, force: true })
})
})

View File

@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { parse, ParseError, printParseErrorCode } from "jsonc-parser"
export interface JsoncParseResult<T> {
@@ -64,3 +65,16 @@ export function detectConfigFile(basePath: string): {
}
return { format: "none", path: jsonPath }
}
const PLUGIN_CONFIG_NAMES = ["oh-my-openagent", "oh-my-opencode"] as const
export function detectPluginConfigFile(dir: string): {
format: "json" | "jsonc" | "none"
path: string
} {
for (const name of PLUGIN_CONFIG_NAMES) {
const result = detectConfigFile(join(dir, name))
if (result.format !== "none") return result
}
return { format: "none", path: join(dir, PLUGIN_CONFIG_NAMES[0] + ".json") }
}

View File

@@ -4,7 +4,7 @@ import { join } from "path"
import { BUILTIN_SERVERS } from "./constants"
import type { ResolvedServer } from "./types"
import { getOpenCodeConfigDir } from "../../shared"
import { parseJsonc, detectConfigFile } from "../../shared/jsonc-parser"
import { parseJsonc, detectConfigFile, detectPluginConfigFile } from "../../shared/jsonc-parser"
interface LspEntry {
disabled?: boolean
@@ -38,8 +38,8 @@ export function getConfigPaths(): { project: string; user: string; opencode: str
const cwd = process.cwd()
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
return {
project: detectConfigFile(join(cwd, ".opencode", "oh-my-opencode")).path,
user: detectConfigFile(join(configDir, "oh-my-opencode")).path,
project: detectPluginConfigFile(join(cwd, ".opencode")).path,
user: detectPluginConfigFile(configDir).path,
opencode: detectConfigFile(join(configDir, "opencode")).path,
}
}