matrix-plugin/dist/config-loader.js

198 lines
8.3 KiB
JavaScript

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 };
}