diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 223aba61a..118952a31 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -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": { diff --git a/src/cli/doctor/checks/config.ts b/src/cli/doctor/checks/config.ts index 4f8fc549f..26852db8e 100644 --- a/src/cli/doctor/checks/config.ts +++ b/src/cli/doctor/checks/config.ts @@ -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 diff --git a/src/cli/doctor/checks/model-resolution-config.ts b/src/cli/doctor/checks/model-resolution-config.ts index db01cc4e5..b9416955f 100644 --- a/src/cli/doctor/checks/model-resolution-config.ts +++ b/src/cli/doctor/checks/model-resolution-config.ts @@ -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") diff --git a/src/plugin-config.ts b/src/plugin-config.ts index c9f93f10f..e3aadb1c3 100644 --- a/src/plugin-config.ts +++ b/src/plugin-config.ts @@ -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 = diff --git a/src/shared/jsonc-parser.test.ts b/src/shared/jsonc-parser.test.ts index d96a7f6b5..759730d15 100644 --- a/src/shared/jsonc-parser.test.ts +++ b/src/shared/jsonc-parser.test.ts @@ -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 }) + }) +}) diff --git a/src/shared/jsonc-parser.ts b/src/shared/jsonc-parser.ts index c7b2fa749..b7c715ad5 100644 --- a/src/shared/jsonc-parser.ts +++ b/src/shared/jsonc-parser.ts @@ -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 { @@ -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") } +} diff --git a/src/tools/lsp/server-config-loader.ts b/src/tools/lsp/server-config-loader.ts index 7a03499c7..9cc38d9b8 100644 --- a/src/tools/lsp/server-config-loader.ts +++ b/src/tools/lsp/server-config-loader.ts @@ -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, } }