/** * pi-channels — Config from pi SettingsManager. * * Reads the "pi-channels" key from settings via SettingsManager, * which merges global (~/.pi/agent/settings.json) and project * (.pi/settings.json) configs automatically. * * Example settings.json: * { * "pi-channels": { * "adapters": { * "telegram": { * "type": "telegram", * "botToken": "your-telegram-bot-token" * }, * "slack": { * "type": "slack" * } * }, * "slack": { * "appToken": "xapp-...", * "botToken": "xoxb-..." * }, * "routes": { * "ops": { "adapter": "telegram", "recipient": "-100987654321" } * } * } * } */ import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent"; import type { ChannelConfig } from "./types.js"; const SETTINGS_KEY = "pi-channels"; export function loadConfig(cwd: string): ChannelConfig { const agentDir = getAgentDir(); const sm = SettingsManager.create(cwd, agentDir); const global = sm.getGlobalSettings() as Record; const project = sm.getProjectSettings() as Record; const globalCh = global?.[SETTINGS_KEY] ?? {}; const projectCh = project?.[SETTINGS_KEY] ?? {}; // Project overrides global (shallow merge of adapters + routes + bridge) const merged: ChannelConfig = { adapters: { ...(globalCh.adapters ?? {}), ...(projectCh.adapters ?? {}), } as ChannelConfig["adapters"], routes: { ...(globalCh.routes ?? {}), ...(projectCh.routes ?? {}), }, bridge: { ...(globalCh.bridge ?? {}), ...(projectCh.bridge ?? {}), } as ChannelConfig["bridge"], }; return merged; } /** * Read a setting from the "pi-channels" config by dotted key path. * Useful for adapter-specific secrets that shouldn't live in the adapter config block. * * Example: getChannelSetting(cwd, "slack.appToken") reads pi-channels.slack.appToken */ export function getChannelSetting(cwd: string, keyPath: string): unknown { const agentDir = getAgentDir(); const sm = SettingsManager.create(cwd, agentDir); const global = sm.getGlobalSettings() as Record; const project = sm.getProjectSettings() as Record; const globalCh = global?.[SETTINGS_KEY] ?? {}; const projectCh = project?.[SETTINGS_KEY] ?? {}; // Walk the dotted path independently in each scope to avoid // shallow-merge dropping sibling keys from nested objects. function walk(obj: any): unknown { let current: any = obj; for (const part of keyPath.split(".")) { if (current == null || typeof current !== "object") return undefined; current = current[part]; } return current; } // Project overrides global at the leaf level. // Use explicit undefined check so null can be used to unset a global default. const projectValue = walk(projectCh); return projectValue !== undefined ? projectValue : walk(globalCh); }