import fs from "fs"; import path from "path"; import os from "os"; import { log } from "./logger.js"; const CONFIG_FILENAMES = ["matrix.json", "matrix.jsonc"]; function mergeDefaults(raw, envOverrides) { const base = { homeserver: envOverrides.homeserver || "https://matrix.org", userId: envOverrides.userId, accessToken: envOverrides.accessToken, password: envOverrides.password, deviceId: envOverrides.deviceId || "opencode-matrix-plugin", autoJoin: envOverrides.autoJoin !== false, triggerPatterns: envOverrides.triggerPatterns || ["!oc "], ignoreRooms: envOverrides.ignoreRooms || [], ignoreUsers: envOverrides.ignoreUsers || [], allowedUsers: envOverrides.allowedUsers || [], formatHtml: envOverrides.formatHtml || false, threadIsolation: envOverrides.threadIsolation !== false, respondToThreadReplies: envOverrides.respondToThreadReplies !== false, rateLimitSeconds: envOverrides.rateLimitSeconds || 5, botName: envOverrides.botName || "opencode", storagePath: envOverrides.storagePath, }; if (!raw) return base; return { homeserver: raw.homeserver || base.homeserver, userId: raw.userId || base.userId, accessToken: raw.accessToken || base.accessToken, password: raw.password || base.password, deviceId: raw.deviceId || base.deviceId, autoJoin: raw.autoJoin !== undefined ? raw.autoJoin : base.autoJoin, triggerPatterns: raw.triggerPatterns || base.triggerPatterns, ignoreRooms: raw.ignoreRooms || base.ignoreRooms, ignoreUsers: raw.ignoreUsers || base.ignoreUsers, allowedUsers: raw.allowedUsers || base.allowedUsers, formatHtml: raw.formatHtml !== undefined ? raw.formatHtml : base.formatHtml, threadIsolation: raw.threadIsolation !== undefined ? raw.threadIsolation : base.threadIsolation, respondToThreadReplies: raw.respondToThreadReplies !== undefined ? raw.respondToThreadReplies : base.respondToThreadReplies, rateLimitSeconds: raw.rateLimitSeconds || base.rateLimitSeconds, botName: raw.botName || base.botName, storagePath: raw.storagePath || base.storagePath, }; } function tryReadConfig(dir) { for (const filename of CONFIG_FILENAMES) { const filepath = path.join(dir, filename); if (!fs.existsSync(filepath)) continue; try { const content = fs.readFileSync(filepath, "utf-8"); const parsed = JSON.parse(content); if (typeof parsed === "object" && parsed !== null) { return parsed; } } catch (e) { log(`Failed to read config: ${filepath}`); return null; } } return null; } function findProjectConfigDir() { let dir = process.cwd(); const root = path.parse(dir).root; while (dir && dir !== root) { const opencodeDir = path.join(dir, ".opencode"); if (fs.existsSync(opencodeDir) && fs.statSync(opencodeDir).isDirectory()) { return opencodeDir; } dir = path.dirname(dir); } const currentOpencode = path.join(process.cwd(), ".opencode"); if (fs.existsSync(currentOpencode) && fs.statSync(currentOpencode).isDirectory()) { return currentOpencode; } return null; } function findGlobalConfigDir() { return path.join(os.homedir(), ".config", "opencode"); } const DEFAULT_CONFIG_CONTENT = JSON.stringify({ homeserver: "https://matrix.org", userId: "", accessToken: "", password: "", deviceId: "opencode-matrix-plugin", autoJoin: true, triggerPatterns: ["!oc "], ignoreRooms: [], ignoreUsers: [], allowedUsers: [], formatHtml: false, threadIsolation: true, respondToThreadReplies: true, rateLimitSeconds: 5, botName: "opencode", enabled: true, }, null, 2); async function ensureGlobalConfig(globalConfigDir) { log(`ensureGlobalConfig called, dir=${globalConfigDir}`); for (const filename of CONFIG_FILENAMES) { const filepath = path.join(globalConfigDir, filename); if (fs.existsSync(filepath)) { log(`config already exists: ${filepath}`); return filepath; } } log("no config found, creating default"); try { log("mkdirSync globalConfigDir"); fs.mkdirSync(globalConfigDir, { recursive: true }); const filepath = path.join(globalConfigDir, "matrix.json"); log("writing file: " + filepath); fs.writeFileSync(filepath, DEFAULT_CONFIG_CONTENT, { mode: 0o600 }); log("Created default config: " + filepath); return filepath; } catch (err) { log("Failed to create default config: " + err); return null; } } export async function loadConfig(pluginOptions = {}) { pluginOptions = pluginOptions || {}; const envOverrides = { homeserver: process.env.MATRIX_HOMESERVER, userId: process.env.MATRIX_USER_ID, accessToken: process.env.MATRIX_ACCESS_TOKEN, password: process.env.MATRIX_PASSWORD, deviceId: process.env.MATRIX_DEVICE_ID || undefined, triggerPatterns: process.env.MATRIX_TRIGGER ? [process.env.MATRIX_TRIGGER] : undefined, allowedUsers: process.env.MATRIX_ALLOWED_USERS ? process.env.MATRIX_ALLOWED_USERS.split(",").map(s => s.trim()).filter(Boolean) : undefined, storagePath: process.env.MATRIX_STORAGE_PATH, }; const projectConfigDir = findProjectConfigDir(); log(`projectConfigDir=${projectConfigDir}`); let rawConfig = null; let configPath = null; if (projectConfigDir) { const projectConfig = tryReadConfig(projectConfigDir); log(`projectConfig=${projectConfig ? "found" : "null"}`); if (projectConfig) { rawConfig = projectConfig; const foundFile = CONFIG_FILENAMES.find((f) => fs.existsSync(path.join(projectConfigDir, f))); if (foundFile) { configPath = path.join(projectConfigDir, foundFile); } } } const globalConfigDir = findGlobalConfigDir(); const globalConfig = tryReadConfig(globalConfigDir); log(`globalConfigDir=${globalConfigDir}`); log(`globalConfig=${globalConfig ? "found" : "null"}`); log(`rawConfig=${rawConfig ? "found" : "null"}`); if (!rawConfig && globalConfig) { rawConfig = globalConfig; configPath = path.join(globalConfigDir, "matrix.json"); log("using global config"); } if (!rawConfig) { log("no config found, calling ensureGlobalConfig"); const createdPath = await ensureGlobalConfig(globalConfigDir); log(`ensureGlobalConfig returned=${createdPath}`); if (createdPath) { configPath = createdPath; } } if (rawConfig && rawConfig.enabled === false) { return { options: { ...mergeDefaults(null, envOverrides), homeserver: "" }, configPath, }; } const merged = mergeDefaults(rawConfig, envOverrides); const finalOptions = { homeserver: pluginOptions.homeserver || merged.homeserver, userId: pluginOptions.userId || merged.userId, accessToken: pluginOptions.accessToken || merged.accessToken, password: pluginOptions.password || merged.password, deviceId: pluginOptions.deviceId || merged.deviceId, autoJoin: pluginOptions.autoJoin !== undefined ? pluginOptions.autoJoin : merged.autoJoin, triggerPatterns: pluginOptions.triggerPatterns || merged.triggerPatterns, ignoreRooms: pluginOptions.ignoreRooms || merged.ignoreRooms, ignoreUsers: pluginOptions.ignoreUsers || merged.ignoreUsers, allowedUsers: pluginOptions.allowedUsers || merged.allowedUsers, formatHtml: pluginOptions.formatHtml !== undefined ? pluginOptions.formatHtml : merged.formatHtml, threadIsolation: pluginOptions.threadIsolation !== undefined ? pluginOptions.threadIsolation : merged.threadIsolation, respondToThreadReplies: pluginOptions.respondToThreadReplies !== undefined ? pluginOptions.respondToThreadReplies : merged.respondToThreadReplies, rateLimitSeconds: pluginOptions.rateLimitSeconds || merged.rateLimitSeconds, botName: pluginOptions.botName || merged.botName, storagePath: pluginOptions.storagePath || merged.storagePath, }; return { options: finalOptions, configPath }; }