Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as os from 'node:os';
import * as path from 'node:path';

/** Agent engine backing a bot. */
export type EngineName = 'claude' | 'kimi' | 'codex';
export type EngineName = 'claude' | 'kimi' | 'codex' | 'opencode';

/** Shared config fields used by MessageBridge and Executors (platform-agnostic). */
export interface BotConfigBase {
Expand Down Expand Up @@ -40,6 +40,8 @@ export interface BotConfigBase {
};
/** Codex-specific overrides. Populated only when engine === 'codex'. */
codex?: CodexBotConfig;
/** OpenCode-specific overrides. Populated only when engine === 'opencode'. */
opencode?: OpenCodeBotConfig;
/**
* Stage 4 — opt-in to the persistent Claude process pool. When enabled,
* each chatId is backed by a long-lived Claude Code process (managed by
Expand Down Expand Up @@ -78,6 +80,15 @@ export interface CodexBotConfig {
env?: Record<string, string>;
}

export interface OpenCodeBotConfig {
executable?: string;
model?: string;
contextWindow?: number;
dangerouslySkipPermissions?: boolean;
extraArgs?: string[];
env?: Record<string, string>;
}

/** Feishu bot config (extends base with Feishu credentials). */
export interface BotConfig extends BotConfigBase {
feishu: {
Expand Down Expand Up @@ -182,11 +193,22 @@ export interface CodexJsonConfig {
env?: Record<string, string>;
}

/** OpenCode-specific overrides in bots.json. */
export interface OpenCodeJsonConfig {
executable?: string;
model?: string;
contextWindow?: number;
dangerouslySkipPermissions?: boolean;
extraArgs?: string[];
env?: Record<string, string>;
}

/** Fields shared across all bot JSON entries (engine selection and engine overrides). */
interface EngineJsonFields {
engine?: EngineName;
kimi?: KimiJsonConfig;
codex?: CodexJsonConfig;
opencode?: OpenCodeJsonConfig;
}

export interface FeishuBotJsonEntry extends EngineJsonFields {
Expand All @@ -212,6 +234,7 @@ export interface FeishuBotJsonEntry extends EngineJsonFields {

function feishuBotFromJson(entry: FeishuBotJsonEntry): BotConfig {
const codex = buildCodexConfig(entry.codex);
const opencode = buildOpenCodeConfig(entry.opencode);
return {
name: entry.name,
...(entry.description ? { description: entry.description } : {}),
Expand All @@ -224,6 +247,7 @@ function feishuBotFromJson(entry: FeishuBotJsonEntry): BotConfig {
...(entry.engine ? { engine: entry.engine } : {}),
...(entry.kimi ? { kimi: entry.kimi } : {}),
...(codex ? { codex } : {}),
...(opencode ? { opencode } : {}),
feishu: {
appId: entry.feishuAppId,
appSecret: entry.feishuAppSecret,
Expand Down Expand Up @@ -254,6 +278,7 @@ export interface TelegramBotJsonEntry extends EngineJsonFields {

function telegramBotFromJson(entry: TelegramBotJsonEntry): TelegramBotConfig {
const codex = buildCodexConfig(entry.codex);
const opencode = buildOpenCodeConfig(entry.opencode);
return {
name: entry.name,
...(entry.description ? { description: entry.description } : {}),
Expand All @@ -265,6 +290,7 @@ function telegramBotFromJson(entry: TelegramBotJsonEntry): TelegramBotConfig {
...(entry.engine ? { engine: entry.engine } : {}),
...(entry.kimi ? { kimi: entry.kimi } : {}),
...(codex ? { codex } : {}),
...(opencode ? { opencode } : {}),
telegram: {
botToken: entry.telegramBotToken,
},
Expand Down Expand Up @@ -292,6 +318,7 @@ export interface WebBotJsonEntry extends EngineJsonFields {

export function webBotFromJson(entry: WebBotJsonEntry): BotConfigBase {
const codex = buildCodexConfig(entry.codex);
const opencode = buildOpenCodeConfig(entry.opencode);
return {
name: entry.name,
...(entry.description ? { description: entry.description } : {}),
Expand All @@ -303,6 +330,7 @@ export function webBotFromJson(entry: WebBotJsonEntry): BotConfigBase {
...(entry.engine ? { engine: entry.engine } : {}),
...(entry.kimi ? { kimi: entry.kimi } : {}),
...(codex ? { codex } : {}),
...(opencode ? { opencode } : {}),
claude: buildClaudeConfig(entry),
};
}
Expand All @@ -325,12 +353,14 @@ export interface WechatBotJsonEntry extends EngineJsonFields {

function wechatBotFromJson(entry: WechatBotJsonEntry): WechatBotConfig {
const codex = buildCodexConfig(entry.codex);
const opencode = buildOpenCodeConfig(entry.opencode);
return {
name: entry.name,
...(entry.description ? { description: entry.description } : {}),
...(entry.engine ? { engine: entry.engine } : {}),
...(entry.kimi ? { kimi: entry.kimi } : {}),
...(codex ? { codex } : {}),
...(opencode ? { opencode } : {}),
wechat: {
ilinkBaseUrl: entry.ilinkBaseUrl,
botToken: entry.wechatBotToken,
Expand Down Expand Up @@ -376,14 +406,27 @@ function buildCodexConfig(entry?: CodexJsonConfig): BotConfigBase['codex'] | und
return Object.keys(cfg).length > 0 ? cfg : undefined;
}

function buildOpenCodeConfig(entry?: OpenCodeJsonConfig): BotConfigBase['opencode'] | undefined {
const cfg: BotConfigBase['opencode'] = {
...(process.env.OPENCODE_EXECUTABLE_PATH ? { executable: process.env.OPENCODE_EXECUTABLE_PATH } : {}),
...(process.env.OPENCODE_MODEL ? { model: process.env.OPENCODE_MODEL } : {}),
...(process.env.OPENCODE_CONTEXT_WINDOW ? { contextWindow: parseInt(process.env.OPENCODE_CONTEXT_WINDOW, 10) } : {}),
...(process.env.OPENCODE_SKIP_PERMISSIONS === 'true' ? { dangerouslySkipPermissions: true } : {}),
...(entry ?? {}),
};
return Object.keys(cfg).length > 0 ? cfg : undefined;
}

// --- Single-bot env var mode ---

function feishuBotFromEnv(): BotConfig {
const codex = buildCodexConfig();
const opencode = buildOpenCodeConfig();
return {
name: 'default',
...(process.env.METABOT_ENGINE ? { engine: process.env.METABOT_ENGINE as EngineName } : {}),
...(codex ? { codex } : {}),
...(opencode ? { opencode } : {}),
feishu: {
appId: required('FEISHU_APP_ID'),
appSecret: required('FEISHU_APP_SECRET'),
Expand All @@ -402,10 +445,12 @@ function feishuBotFromEnv(): BotConfig {

function telegramBotFromEnv(): TelegramBotConfig {
const codex = buildCodexConfig();
const opencode = buildOpenCodeConfig();
return {
name: 'telegram-default',
...(process.env.METABOT_ENGINE ? { engine: process.env.METABOT_ENGINE as EngineName } : {}),
...(codex ? { codex } : {}),
...(opencode ? { opencode } : {}),
telegram: {
botToken: required('TELEGRAM_BOT_TOKEN'),
},
Expand All @@ -423,10 +468,12 @@ function telegramBotFromEnv(): TelegramBotConfig {

function wechatBotFromEnv(): WechatBotConfig {
const codex = buildCodexConfig();
const opencode = buildOpenCodeConfig();
return {
name: 'wechat-default',
...(process.env.METABOT_ENGINE ? { engine: process.env.METABOT_ENGINE as EngineName } : {}),
...(codex ? { codex } : {}),
...(opencode ? { opencode } : {}),
wechat: {
botToken: process.env.WECHAT_BOT_TOKEN || undefined,
},
Expand Down
6 changes: 5 additions & 1 deletion src/engines/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Engine, EngineName } from './types.js';
import { ClaudeEngine } from './claude/index.js';
import { KimiEngine } from './kimi/index.js';
import { CodexEngine } from './codex/index.js';
import { OpenCodeEngine } from './opencode/index.js';

/**
* Create an Engine for the given bot config.
Expand All @@ -26,6 +27,8 @@ export function createEngine(
return new KimiEngine(config, logger);
case 'codex':
return new CodexEngine(config, logger);
case 'opencode':
return new OpenCodeEngine(config, logger);
default: {
const _exhaustive: never = name;
throw new Error(`Unknown engine: ${_exhaustive}`);
Expand All @@ -38,14 +41,15 @@ export function resolveEngineName(config: BotConfigBase): EngineName {
const explicit = config.engine;
if (explicit) return explicit;
const envDefault = process.env.METABOT_ENGINE as EngineName | undefined;
if (envDefault === 'claude' || envDefault === 'kimi' || envDefault === 'codex') return envDefault;
if (envDefault === 'claude' || envDefault === 'kimi' || envDefault === 'codex' || envDefault === 'opencode') return envDefault;
return 'claude';
}

export type { Engine, EngineName, Executor } from './types.js';
export { ClaudeEngine } from './claude/index.js';
export { KimiEngine } from './kimi/index.js';
export { CodexEngine } from './codex/index.js';
export { OpenCodeEngine } from './opencode/index.js';

// Re-export shared types and classes currently used by the bridge and web/api layers.
// Moving these behind the engine boundary lets consumers import from a single place.
Expand Down
Loading