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
+141
View File
@@ -3736,6 +3736,147 @@
}, },
"additionalProperties": false "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": { "babysitting": {
"type": "object", "type": "object",
"properties": { "properties": {
+5 -5
View File
@@ -2,15 +2,15 @@ import { readFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { OhMyOpenCodeConfigSchema } from "../../../config" 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 { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
import type { CheckResult, DoctorIssue } from "../types" import type { CheckResult, DoctorIssue } from "../types"
import { loadAvailableModelsFromCache } from "./model-resolution-cache" import { loadAvailableModelsFromCache } from "./model-resolution-cache"
import { getModelResolutionInfoWithOverrides } from "./model-resolution" import { getModelResolutionInfoWithOverrides } from "./model-resolution"
import type { OmoConfig } from "./model-resolution-types" import type { OmoConfig } from "./model-resolution-types"
const USER_CONFIG_BASE = join(getOpenCodeConfigDir({ binary: "opencode" }), PACKAGE_NAME) const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME) const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
interface ConfigValidationResult { interface ConfigValidationResult {
exists: boolean exists: boolean
@@ -21,10 +21,10 @@ interface ConfigValidationResult {
} }
function findConfigPath(): string | null { function findConfigPath(): string | null {
const projectConfig = detectConfigFile(PROJECT_CONFIG_BASE) const projectConfig = detectPluginConfigFile(PROJECT_CONFIG_DIR)
if (projectConfig.format !== "none") return projectConfig.path 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 if (userConfig.format !== "none") return userConfig.path
return null return null
@@ -1,17 +1,13 @@
import { readFileSync } from "node:fs" import { readFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { detectConfigFile, getOpenCodeConfigPaths, parseJsonc } from "../../../shared" import { detectPluginConfigFile, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
import type { OmoConfig } from "./model-resolution-types" import type { OmoConfig } from "./model-resolution-types"
const PACKAGE_NAME = "oh-my-opencode" const USER_CONFIG_DIR = getOpenCodeConfigPaths({ binary: "opencode", version: null }).configDir
const USER_CONFIG_BASE = join( const PROJECT_CONFIG_DIR = join(process.cwd(), ".opencode")
getOpenCodeConfigPaths({ binary: "opencode", version: null }).configDir,
PACKAGE_NAME
)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
export function loadOmoConfig(): OmoConfig | null { export function loadOmoConfig(): OmoConfig | null {
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE) const projectDetected = detectPluginConfigFile(PROJECT_CONFIG_DIR)
if (projectDetected.format !== "none") { if (projectDetected.format !== "none") {
try { try {
const content = readFileSync(projectDetected.path, "utf-8") 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") { if (userDetected.format !== "none") {
try { try {
const content = readFileSync(userDetected.path, "utf-8") const content = readFileSync(userDetected.path, "utf-8")
+6 -7
View File
@@ -7,7 +7,7 @@ import {
getOpenCodeConfigDir, getOpenCodeConfigDir,
addConfigLoadError, addConfigLoadError,
parseJsonc, parseJsonc,
detectConfigFile, detectPluginConfigFile,
migrateConfigFile, migrateConfigFile,
} from "./shared"; } from "./shared";
@@ -162,20 +162,19 @@ export function loadPluginConfig(
): OhMyOpenCodeConfig { ): OhMyOpenCodeConfig {
// User-level config path - prefer .jsonc over .json // User-level config path - prefer .jsonc over .json
const configDir = getOpenCodeConfigDir({ binary: "opencode" }); const configDir = getOpenCodeConfigDir({ binary: "opencode" });
const userBasePath = path.join(configDir, "oh-my-opencode"); const userDetected = detectPluginConfigFile(configDir);
const userDetected = detectConfigFile(userBasePath);
const userConfigPath = const userConfigPath =
userDetected.format !== "none" userDetected.format !== "none"
? userDetected.path ? userDetected.path
: userBasePath + ".json"; : path.join(configDir, "oh-my-openagent.json");
// Project-level config path - prefer .jsonc over .json // Project-level config path - prefer .jsonc over .json
const projectBasePath = path.join(directory, ".opencode", "oh-my-opencode"); const projectBasePath = path.join(directory, ".opencode");
const projectDetected = detectConfigFile(projectBasePath); const projectDetected = detectPluginConfigFile(projectBasePath);
const projectConfigPath = const projectConfigPath =
projectDetected.format !== "none" projectDetected.format !== "none"
? projectDetected.path ? projectDetected.path
: projectBasePath + ".json"; : path.join(projectBasePath, "oh-my-openagent.json");
// Load user config first (base) // Load user config first (base)
let config: OhMyOpenCodeConfig = let config: OhMyOpenCodeConfig =
+82 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" 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 { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
@@ -264,3 +264,84 @@ describe("detectConfigFile", () => {
expect(result.format).toBe("none") 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 })
})
})
+14
View File
@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from "node:fs" import { existsSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { parse, ParseError, printParseErrorCode } from "jsonc-parser" import { parse, ParseError, printParseErrorCode } from "jsonc-parser"
export interface JsoncParseResult<T> { export interface JsoncParseResult<T> {
@@ -64,3 +65,16 @@ export function detectConfigFile(basePath: string): {
} }
return { format: "none", path: jsonPath } 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") }
}
+3 -3
View File
@@ -4,7 +4,7 @@ import { join } from "path"
import { BUILTIN_SERVERS } from "./constants" import { BUILTIN_SERVERS } from "./constants"
import type { ResolvedServer } from "./types" import type { ResolvedServer } from "./types"
import { getOpenCodeConfigDir } from "../../shared" import { getOpenCodeConfigDir } from "../../shared"
import { parseJsonc, detectConfigFile } from "../../shared/jsonc-parser" import { parseJsonc, detectConfigFile, detectPluginConfigFile } from "../../shared/jsonc-parser"
interface LspEntry { interface LspEntry {
disabled?: boolean disabled?: boolean
@@ -38,8 +38,8 @@ export function getConfigPaths(): { project: string; user: string; opencode: str
const cwd = process.cwd() const cwd = process.cwd()
const configDir = getOpenCodeConfigDir({ binary: "opencode" }) const configDir = getOpenCodeConfigDir({ binary: "opencode" })
return { return {
project: detectConfigFile(join(cwd, ".opencode", "oh-my-opencode")).path, project: detectPluginConfigFile(join(cwd, ".opencode")).path,
user: detectConfigFile(join(configDir, "oh-my-opencode")).path, user: detectPluginConfigFile(configDir).path,
opencode: detectConfigFile(join(configDir, "opencode")).path, opencode: detectConfigFile(join(configDir, "opencode")).path,
} }
} }