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
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ More: [Roles & teams](https://deepcoldy.github.io/botmux/en/roles) · [File sand

Switch with `cliId` in `bots.json`. **20+ adapters**, spanning local CLIs (process-isolated, reachable via `tmux attach`) and API / cloud agents (e.g. Mira, riff — reached over API / remote, not a local process; mojo is API-driven but executes tools on the bot host by default, set cloud: true for the remote sandbox). Representative ones:

`claude-code` · `codex` · `gemini` · `cursor` · `opencode` · `opencode2` · `antigravity` · `copilot` · `grok` · `kimi` · `kiro-cli` · `reasonix` · `dsh` · `aiden` · `coco` (TRAE) · `hermes` · `ebsd` · `mira` · `riff` (cloud agent) … · `mojo` (API-driven, host execution by default) …
`claude-code` · `codex` · `gemini` · `cursor` · `opencode` · `opencode2` · `antigravity` · `copilot` · `grok` · `kimi` · `kiro-cli` · `reasonix` · `dsh` · `aiden` · `coco` (TRAE) · `hermes` · `ebsd` · `mira` · `riff` (cloud agent) … · `mojo` (API-driven, host execution by default) · `minimax` (MiniMax `mmx text repl`; region set by `mmx auth login --region cn|global`, isolate multiple regions on one host via per-bot `env` `MMX_CONFIG_DIR`)

The `ebsd` adapter uses a dedicated external service identity and native OMP session storage. Operators must provide the Diag Gateway token and ByteCloud service account through permission-restricted files, never through `bots.json`.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ npm 包内带的是**同一个自包含二进制**(按 os/arch 只装匹配的

`bots.json` 里用 `cliId` 一键切换。**20+ 适配器**,覆盖本地 CLI(进程隔离,`tmux attach` 可直连)和 API / 云 Agent(如 Mira、riff——通过 API / 远端接入,非本地进程;mojo 为 API 驱动、默认在宿主机执行工具,可配 cloud: true 走云沙箱)。代表项:

`claude-code` · `codex` · `gemini` · `cursor` · `opencode` · `opencode2` · `antigravity` · `copilot` · `grok` · `kimi` · `kiro-cli` · `reasonix` · `dsh` · `aiden` · `coco`(TRAE) · `hermes` · `ebsd` · `mira` · `riff`(云 Agent) … · `mojo`(API 驱动,默认宿主机执行) …
`claude-code` · `codex` · `gemini` · `cursor` · `opencode` · `opencode2` · `antigravity` · `copilot` · `grok` · `kimi` · `kiro-cli` · `reasonix` · `dsh` · `aiden` · `coco`(TRAE) · `hermes` · `ebsd` · `mira` · `riff`(云 Agent) … · `mojo`(API 驱动,默认宿主机执行) · `minimax`(MiniMax `mmx text repl`;区域由 `mmx auth login --region cn|global` 决定,同机多区用 per-bot `env` 的 `MMX_CONFIG_DIR` 隔离)

`ebsd` 使用独立的外部服务身份和原生 OMP 会话目录;部署方必须通过受限权限文件配置 Diag Gateway token 与 ByteCloud service account,不能把密钥写入 `bots.json`。

Expand Down
125 changes: 125 additions & 0 deletions src/adapters/cli/minimax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { resolveCommand } from './registry.js';
import type { CliAdapter, PtyHandle } from './types.js';

import { delay } from '../../utils/timing.js';

/**
* MiniMax CLI (`mmx`, package `mmx-cli`) adapter.
*
* `mmx` is MiniMax's multimodal generation CLI. Its ONLY interactive,
* multi-turn surface is `mmx text repl` — a readline-style chat prompt
* (banner "MiniMax Chat REPL", a boxed `> ` input line, streamed model
* output, `/exit` to quit). It hard-requires a TTY (`repl requires an
* interactive terminal`), which the PTY/tmux backend provides.
*
* IMPORTANT — capability envelope: `text repl` is a pure chat/generation
* loop with NO shell or file-tool surface, so this bot CANNOT run the
* `botmux send` wrapper the way agentic CLIs do. botmux relays its answers
* the same way it does for other tool-less TUIs: quiescence detection +
* headless screen capture of the streamed reply. Hence no `skillsDir`
* (nothing to install into) and `injectsSessionContext: true` to suppress
* the inline routing/identity envelope it cannot act on (see below). The
* first user prompt is written to stdin after idle detection — `mmx text
* repl` has no launch-time `-i`/prompt flag, and its readline input accepts
* a single line only, so `writeInput` folds newlines to spaces (see below).
*
* Auth: `mmx auth login` writes `~/.mmx/config.json` (the whole `~/.mmx`
* dir is the authPath so a sandboxed first login persists). The `--api-key`
* flag and `MMX_CONFIG_DIR` env are alternative resolution paths; we do not
* bake a key into argv.
*
* Region (CN vs. global) — supported, resolved by mmx itself:
* `mmx` picks the API host by precedence `--base-url` > `--region` >
* the `region` field in `~/.mmx/config.json` (written at login, default
* `global`). `cn` → api.minimaxi.com, `global` → api.minimax.io. This
* adapter does NOT pass `--region`, so a bot follows whichever region you
* chose at `mmx auth login`:
* mmx auth login --api-key sk-... --region cn # China
* mmx auth login --api-key sk-... --region global # international
*
* Running a CN bot and a global bot on the SAME host: `~/.mmx` holds one
* region, so give each bot its own credential dir via the per-bot `env`
* field in bots.json (`MMX_CONFIG_DIR`), each logged into its own region:
* bot-cn: env: { "MMX_CONFIG_DIR": "~/.mmx-cn" }
* bot-global: env: { "MMX_CONFIG_DIR": "~/.mmx-global" }
* Prepare each once, e.g.
* MMX_CONFIG_DIR=~/.mmx-cn mmx auth login --api-key sk-... --region cn
* (Under the file sandbox, `authPaths` is the static `~/.mmx`; a
* redirected MMX_CONFIG_DIR would additionally need that dir exposed —
* irrelevant to the default non-sandboxed setup.)
*/
export function createMinimaxAdapter(pathOverride?: string): CliAdapter {
// resolvedBin is lazy: setup constructs adapters only to read static
// modelChoices and must not shell out (see resolveCommand); the binary
// path is a spawn-time concern.
const rawBin = pathOverride ?? 'mmx';
let cachedBin: string | undefined;
return {
id: 'minimax',
// Whole dir, not just config.json: the file may not exist yet on a fresh
// login inside the sandbox, and a single-file carve-out would be skipped
// (bwrap can't bind a missing source) — see CLAUDE.md sandbox note (3).
authPaths: ['~/.mmx'],
get resolvedBin(): string { return (cachedBin ??= resolveCommand(rawBin)); },

buildArgs({ model }) {
// `mmx text repl` keeps no daemon-resumable session state, so we always
// start fresh (like gemini). Auth comes from `mmx auth login` (persisted
// in ~/.mmx) or the MMX_CONFIG_DIR / --api-key escape hatches.
const args = ['text', 'repl'];
if (model && model.trim()) {
args.push('--model', model.trim());
}
return args;
},

async writeInput(pty: PtyHandle, content: string) {
// `mmx text repl` is a single-line readline prompt. Embedded newlines are
// NOT multi-line input: verified on mmx 1.0.25 that its readline SWALLOWS
// interior '\n'/'\r' (the whole payload accretes onto one input line) and
// only the FINAL terminator submits — so a raw multi-line write submits
// once but the model receives every line jammed together with no
// separator (e.g. "line1line2"), and a leading-only routing/scaffold line
// makes the real question invisible → empirically an empty/garbled reply.
// It is a plain readline, not an Ink bracketed-paste widget: it never
// requests ?2004h, so paste markers (\e[200~…) are echoed literally to the
// model, not consumed. There is therefore no way to preserve hard line
// breaks here; fold every run of whitespace-with-newline down to a single
// space so the model at least sees the full text as one coherent line.
const flattened = content.replace(/\s*\n\s*/g, ' ').replace(/[ \t]+/g, ' ').trim();
// Prefer the tmux literal-send + Enter path; fall back to raw write + CR.
if (pty.sendText && pty.sendSpecialKeys) {
pty.sendText(flattened);
await delay(200);
pty.sendSpecialKeys('Enter');
} else {
pty.write(flattened);
await delay(1000);
pty.write('\r');
}
},

completionPattern: undefined, // quiescence only — no explicit marker
readyPattern: undefined, // rely on quiescence; '> ' prompt is too generic
// Tool-less chat loop: it has no shell/file surface to act on botmux's
// routing/@/send hints, and (verified) an inline <botmux_routing> block on
// the first turn just becomes noise the model apologizes about. So set
// `injectsSessionContext: true` — the same suppression switch mira / riff /
// mojo use — which makes session-manager skip the inline routing / identity
// / session_id envelope entirely. Unlike those three we push NOTHING back
// via a system-prompt flag (`mmx text repl` has none), so `systemHints` is
// also empty: the bot runs as a plain chat model with no botmux scaffolding.
injectsSessionContext: true,
systemHints: [],
// mmx repl redraws its input line in place (cursor hide + line clears)
// but does NOT switch into the alternate screen buffer.
altScreen: false,
modelChoices: [
'MiniMax-M3',
'MiniMax-M2.7',
'MiniMax-M2.7-highspeed',
],
};
}

export const create = createMinimaxAdapter;
6 changes: 5 additions & 1 deletion src/adapters/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { createReasonixAdapter } from './reasonix.js';
import { createDshAdapter } from './dsh.js';
import { createDshTuiAdapter } from './dsh-tui.js';
import { createMojoAdapter } from './mojo.js';
import { createMinimaxAdapter } from './minimax.js';

/**
* The first CLI executable (or nested runner dependency) before shell
Expand Down Expand Up @@ -86,6 +87,8 @@ const RAW_CLI_EXECUTABLES: Readonly<Record<CliId, string | undefined>> = {
// Declaring it lets `botmux setup` fail fast on a missing install instead of
// letting the first IM message die with ENOENT.
mojo: 'mojo',
// MiniMax CLI — `mmx text repl` is the interactive chat surface.
minimax: 'mmx',
};

/**
Expand Down Expand Up @@ -224,7 +227,7 @@ export async function createCliAdapter(id: CliId, pathOverride?: string): Promis
return adapter;
}

export { createClaudeCodeAdapter, createSeedAdapter, createRelayAdapter, createAidenAdapter, createCocoAdapter, createCodexAdapter, createCodexAppAdapter, createCursorAdapter, createGeminiAdapter, createGeniusAdapter, createOpenCodeAdapter, createOpenCode2Adapter, createAntigravityAdapter, createMtrAdapter, createHermesAdapter, createMiraAdapter, createMirAdapter, createTraexAdapter, createPiAdapter, createCopilotAdapter, createOhMyPiAdapter, createEbsdAdapter, createKimiAdapter, createGrokAdapter, createKiroCliAdapter, createRiffAdapter, createReasonixAdapter, createDshAdapter, createDshTuiAdapter, createMojoAdapter };
export { createClaudeCodeAdapter, createSeedAdapter, createRelayAdapter, createAidenAdapter, createCocoAdapter, createCodexAdapter, createCodexAppAdapter, createCursorAdapter, createGeminiAdapter, createGeniusAdapter, createOpenCodeAdapter, createOpenCode2Adapter, createAntigravityAdapter, createMtrAdapter, createHermesAdapter, createMiraAdapter, createMirAdapter, createTraexAdapter, createPiAdapter, createCopilotAdapter, createOhMyPiAdapter, createEbsdAdapter, createKimiAdapter, createGrokAdapter, createKiroCliAdapter, createRiffAdapter, createReasonixAdapter, createDshAdapter, createDshTuiAdapter, createMojoAdapter, createMinimaxAdapter };

/** Synchronous version for use in worker process. */
export function createCliAdapterSync(id: CliId, pathOverride?: string): CliAdapter {
Expand Down Expand Up @@ -259,6 +262,7 @@ export function createCliAdapterSync(id: CliId, pathOverride?: string): CliAdapt
case 'dsh': return createDshAdapter(pathOverride);
case 'dsh-tui': return createDshTuiAdapter(pathOverride);
case 'mojo': return createMojoAdapter(pathOverride);
case 'minimax': return createMinimaxAdapter(pathOverride);
default: throw new Error(`Unknown CLI adapter: ${id}`);
}
}
2 changes: 1 addition & 1 deletion src/adapters/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,4 +684,4 @@ export interface CliAdapter {
buildSessionRenameCommand?(title: string): string;
}

export type CliId = 'claude-code' | 'seed' | 'relay' | 'aiden' | 'coco' | 'codex' | 'codex-app' | 'cursor' | 'gemini' | 'genius' | 'opencode' | 'opencode2' | 'antigravity' | 'mtr' | 'hermes' | 'mira' | 'mir' | 'traex' | 'pi' | 'copilot' | 'oh-my-pi' | 'ebsd' | 'kimi' | 'grok' | 'kiro-cli' | 'riff' | 'reasonix' | 'dsh' | 'dsh-tui' | 'mojo';
export type CliId = 'claude-code' | 'seed' | 'relay' | 'aiden' | 'coco' | 'codex' | 'codex-app' | 'cursor' | 'gemini' | 'genius' | 'opencode' | 'opencode2' | 'antigravity' | 'mtr' | 'hermes' | 'mira' | 'mir' | 'traex' | 'pi' | 'copilot' | 'oh-my-pi' | 'ebsd' | 'kimi' | 'grok' | 'kiro-cli' | 'riff' | 'reasonix' | 'dsh' | 'dsh-tui' | 'mojo' | 'minimax';
1 change: 1 addition & 0 deletions src/im/lark/card-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ const cliDisplayNames: Record<CliId, string> = {
'dsh': 'DeepSeek Harness',
'dsh-tui': 'DeepSeek Harness TUI',
'mojo': 'Mojo',
'minimax': 'MiniMax',
};

export function getCliDisplayName(cliId: CliId): string {
Expand Down
4 changes: 4 additions & 0 deletions src/setup/bot-config-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export const CLI_ID_CHOICES: Record<string, CliId> = {
// (25→reasonix、26→opencode2、27→dsh)。
'28': 'mojo',
'29': 'ebsd',
// 新增 CLI 一律追加到尾部:序号是脚本化 setup(非 TTY 管道喂数字)的稳定接口,
// 插位会让老脚本静默选错 CLI。
'30': 'minimax',
};

const VALID_CLI_IDS: ReadonlySet<string> = new Set(Object.values(CLI_ID_CHOICES));
Expand Down Expand Up @@ -82,6 +85,7 @@ const CLI_DISPLAY_LABELS: Record<CliId, string> = {
'dsh': 'DeepSeek Harness',
'dsh-tui': 'DeepSeek Harness TUI',
'mojo': 'Mojo',
'minimax': 'MiniMax',
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2184,7 +2184,7 @@ let closeRequested = false;
let capturedSpawnCommand: string | null = null;
let deferredTopicOutputTail = '';
const reportedDeferredTopicRoots = new Set<string>();
const CLI_DISPLAY_NAMES: Record<string, string> = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', genius: 'Genius', opencode: 'OpenCode', opencode2: 'OpenCode 2', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi', ebsd: 'ebsd', kimi: 'Kimi', grok: 'Grok Build', 'kiro-cli': 'Kiro', riff: 'Riff', reasonix: 'Reasonix', dsh: 'DeepSeek Harness', 'dsh-tui': 'DeepSeek Harness TUI', mojo: 'Mojo' };
const CLI_DISPLAY_NAMES: Record<string, string> = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', genius: 'Genius', opencode: 'OpenCode', opencode2: 'OpenCode 2', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi', ebsd: 'ebsd', kimi: 'Kimi', grok: 'Grok Build', 'kiro-cli': 'Kiro', riff: 'Riff', reasonix: 'Reasonix', dsh: 'DeepSeek Harness', 'dsh-tui': 'DeepSeek Harness TUI', mojo: 'Mojo', minimax: 'MiniMax' };
function cliName(): string {
return (lastInitConfig?.cliRuntime?.source === 'configured'
? (lastInitConfig.cliRuntime.displayName?.trim() || lastInitConfig.cliRuntime.id)
Expand Down
3 changes: 3 additions & 0 deletions test/cli-id-roster-derivation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ describe('native skill discovery reaches every CLI that ships skills', () => {
// PTY-driven TUI variant of dsh; no skills dir of its own.
'dsh-tui': 'none',
mira: 'none', mir: 'none', copilot: 'none', kimi: 'none', riff: 'none',
// minimax: `mmx text repl` is a tool-less chat loop (no shell/file
// surface, no skillsDir); classifier sees no skillsDir / pluginDir → 'none'.
minimax: 'none',
};
// Two-way key equality. `Record<CliId, …>` looks like tsc enforces
// exhaustiveness, but this file lives in test/ and tsconfig only includes
Expand Down