From 673d68b4b32cb61de57ac2981929427f55a4752e Mon Sep 17 00:00:00 2001 From: lijason1121 Date: Mon, 7 Sep 2026 18:22:22 +0800 Subject: [PATCH 1/4] feat(cli): add MiniMax CLI (mmx) adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `minimax` CLI adapter bridging MiniMax's official CLI (`mmx`, package `mmx-cli`). Its interactive multi-turn surface is `mmx text repl`, a readline-style chat REPL that requires a TTY — provided by the PTY/tmux backend. The repl is a pure chat/generation loop with no shell or file-tool surface, so like mira/riff it relays via quiescence + screen capture rather than the `botmux send` wrapper: no skillsDir, empty systemHints, altScreen=false (it redraws its input line in place, not via the alternate screen buffer). Auth resolves from `mmx auth login` (~/.mmx/config.json); the whole ~/.mmx dir is the authPath so a sandboxed first login persists. Registered per src/adapters/cli/CLAUDE.md checklist: - src/adapters/cli/minimax.ts (new) - CliId union (types.ts) - registry.ts: RAW_CLI_EXECUTABLES + import + export + switch case - worker.ts CLI_DISPLAY_NAMES - im/lark/card-builder.ts cliDisplayNames - setup/bot-config-editor.ts CLI_ID_CHOICES (#30, appended) + CLI_DISPLAY_LABELS Verified: tsc --noEmit clean; cli-adapters + cli-availability unit suites pass (449); end-to-end PTY harness drove a real MiniMax-M3 reply through buildArgs -> spawn -> writeInput -> output. --- src/adapters/cli/minimax.ts | 85 ++++++++++++++++++++++++++++++++++ src/adapters/cli/registry.ts | 6 ++- src/adapters/cli/types.ts | 2 +- src/im/lark/card-builder.ts | 1 + src/setup/bot-config-editor.ts | 4 ++ src/worker.ts | 2 +- 6 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/adapters/cli/minimax.ts diff --git a/src/adapters/cli/minimax.ts b/src/adapters/cli/minimax.ts new file mode 100644 index 0000000000..fa573b825f --- /dev/null +++ b/src/adapters/cli/minimax.ts @@ -0,0 +1,85 @@ +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 no shell routing hints (it can't act on + * them). The first user prompt is written to stdin after idle detection — + * `mmx text repl` has no launch-time `-i`/prompt flag. + * + * 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. + */ +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's repl is a readline-style prompt (not an Ink bracketed-paste + // widget): it echoes typed characters and submits on Enter. Prefer the + // tmux literal-send + Enter path; fall back to raw write + CR. + if (pty.sendText && pty.sendSpecialKeys) { + pty.sendText(content); + await delay(200); + pty.sendSpecialKeys('Enter'); + } else { + pty.write(content); + 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 hints, so inject none (same as mira / riff / mojo). + 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; diff --git a/src/adapters/cli/registry.ts b/src/adapters/cli/registry.ts index f7123327a1..a1874cb789 100644 --- a/src/adapters/cli/registry.ts +++ b/src/adapters/cli/registry.ts @@ -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 @@ -86,6 +87,8 @@ const RAW_CLI_EXECUTABLES: Readonly> = { // 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', }; /** @@ -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 { @@ -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}`); } } diff --git a/src/adapters/cli/types.ts b/src/adapters/cli/types.ts index f8888fa761..b53a2762b9 100644 --- a/src/adapters/cli/types.ts +++ b/src/adapters/cli/types.ts @@ -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'; diff --git a/src/im/lark/card-builder.ts b/src/im/lark/card-builder.ts index ba5688b7cf..86cd32b29c 100644 --- a/src/im/lark/card-builder.ts +++ b/src/im/lark/card-builder.ts @@ -309,6 +309,7 @@ const cliDisplayNames: Record = { 'dsh': 'DeepSeek Harness', 'dsh-tui': 'DeepSeek Harness TUI', 'mojo': 'Mojo', + 'minimax': 'MiniMax', }; export function getCliDisplayName(cliId: CliId): string { diff --git a/src/setup/bot-config-editor.ts b/src/setup/bot-config-editor.ts index fd9016e1aa..b8e21ec75b 100644 --- a/src/setup/bot-config-editor.ts +++ b/src/setup/bot-config-editor.ts @@ -42,6 +42,9 @@ export const CLI_ID_CHOICES: Record = { // (25→reasonix、26→opencode2、27→dsh)。 '28': 'mojo', '29': 'ebsd', + // 新增 CLI 一律追加到尾部:序号是脚本化 setup(非 TTY 管道喂数字)的稳定接口, + // 插位会让老脚本静默选错 CLI。 + '30': 'minimax', }; const VALID_CLI_IDS: ReadonlySet = new Set(Object.values(CLI_ID_CHOICES)); @@ -82,6 +85,7 @@ const CLI_DISPLAY_LABELS: Record = { 'dsh': 'DeepSeek Harness', 'dsh-tui': 'DeepSeek Harness TUI', 'mojo': 'Mojo', + 'minimax': 'MiniMax', }; /** diff --git a/src/worker.ts b/src/worker.ts index 9afd3ea2cd..9d02d47dcb 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -2184,7 +2184,7 @@ let closeRequested = false; let capturedSpawnCommand: string | null = null; let deferredTopicOutputTail = ''; const reportedDeferredTopicRoots = new Set(); -const CLI_DISPLAY_NAMES: Record = { '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 = { '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) From fbe0e3d323b51ddba4ce0e95cef01a3511d0cb49 Mon Sep 17 00:00:00 2001 From: lijason1121 Date: Mon, 7 Sep 2026 20:01:15 +0800 Subject: [PATCH 2/4] docs(minimax): document CN/global region selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Region is resolved by mmx itself (--base-url > --region > ~/.mmx config, default global); the adapter does not pass --region, so a bot follows the region chosen at `mmx auth login`. Document how to run CN and global bots on the same host via per-bot env MMX_CONFIG_DIR (separate credential dirs), and note the sandbox authPaths caveat. Docs/comments only — no behavior change. Also add minimax to the README adapter list. --- README.en.md | 2 +- README.md | 2 +- src/adapters/cli/minimax.ts | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index 944012ac19..ef331c4942 100644 --- a/README.en.md +++ b/README.en.md @@ -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`. diff --git a/README.md b/README.md index e003fd24b6..a7e5bca8a6 100644 --- a/README.md +++ b/README.md @@ -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`。 diff --git a/src/adapters/cli/minimax.ts b/src/adapters/cli/minimax.ts index fa573b825f..30901c542a 100644 --- a/src/adapters/cli/minimax.ts +++ b/src/adapters/cli/minimax.ts @@ -25,6 +25,26 @@ import { delay } from '../../utils/timing.js'; * 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 From e071f354c4b353c375ffd19cabf9c52b3c90de19 Mon Sep 17 00:00:00 2001 From: lijason1121 Date: Tue, 8 Sep 2026 12:14:47 +0800 Subject: [PATCH 3/4] =?UTF-8?q?test(cli):=20=E8=A1=A5=E9=BD=90=20minimax?= =?UTF-8?q?=20=E7=9A=84=20CLI=20id=20roster=20=E5=88=86=E7=B1=BB=E6=96=AD?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit minimax 加入 CliId/ALL_CLI_IDS 后,cli-id-roster-derivation 的双向 key 全等断言因 EXPECTED 缺 minimax 而报 "stale or missing entries in EXPECTED: - minimax",导致 bun run test:bun 一个文件失败。mmx text repl 是无工具的纯 chat loop,无 skillsDir/pluginDir,分类为 'none'。 验证:bun test test/cli-id-roster-derivation.test.ts → 6 pass;tsc --noEmit clean。 --- test/cli-id-roster-derivation.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/cli-id-roster-derivation.test.ts b/test/cli-id-roster-derivation.test.ts index da38eedfde..0aff74b1e1 100644 --- a/test/cli-id-roster-derivation.test.ts +++ b/test/cli-id-roster-derivation.test.ts @@ -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` looks like tsc enforces // exhaustiveness, but this file lives in test/ and tsconfig only includes From 094b1c36e22524619011e298bf87df41fa1029a7 Mon Sep 17 00:00:00 2001 From: lijason1121 Date: Tue, 8 Sep 2026 15:25:49 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(cli):=20minimax=20=E9=A6=96=E8=BD=AE?= =?UTF-8?q?=E6=94=B9=E8=B5=B0=20injectsSessionContext=20+=20=E5=8D=95?= =?UTF-8?q?=E8=A1=8C=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对 mmx text repl 的两处实测行为修正首轮投递: 1. 多行首轮失效。mmx text repl 是单行 readline:实测 mmx 1.0.25 会吞掉内嵌的 \n/\r(整段挤成一行),只有最后一个终止符提交一次。 原实现把多行 prompt 原样写入,模型收到的是各行无分隔拼接的内容, 若开头是 routing/脚手架行,真正的问题被淹没 → 实测首轮回空/答非所问。 它是普通 readline 而非 Ink 的 bracketed-paste 组件:不请求 ?2004h, 粘贴标记 \e[200~ 会被当字面量喂给模型。既然无法保留硬换行, writeInput 现将换行折叠为空格,让模型至少收到完整一行连贯文本。 2. 之前 systemHints 置空但未设 injectsSessionContext,导致 session-manager 仍把完整 // 内联进首轮——而这类 纯 chat loop 无 shell/文件面根本无法响应,实测只会让模型对着 routing 块道歉。改设 injectsSessionContext: true(与 mira/riff/mojo 同一抑制 开关),session-manager 不再内联该外壳;与那三者不同的是 mmx 无 system-prompt 注入入口,故 systemHints 仍空,作为纯 chat 模型运行。 验证(真实 mmx 1.0.25,已登录): - 经真实 buildNewTopicPrompt('minimax') 组装 → 折叠 → 喂 repl,问 7×6 答 42;PtyBackend write+CR 与 tmux send-keys -l+Enter 两条路径均正确。 - 多行 user message 折叠后同样正确落地。 - buildNewTopicPrompt('minimax') 不再含 routing/identity/session_id (与 mira 一致,仅 )。 - tsc --noEmit clean;cli-adapters + cli-availability + roster 455/455; session-discovery/title/skill-injection 93/93。 --- src/adapters/cli/minimax.ts | 38 ++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/adapters/cli/minimax.ts b/src/adapters/cli/minimax.ts index 30901c542a..b61220f651 100644 --- a/src/adapters/cli/minimax.ts +++ b/src/adapters/cli/minimax.ts @@ -17,9 +17,11 @@ import { delay } from '../../utils/timing.js'; * `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 no shell routing hints (it can't act on - * them). The first user prompt is written to stdin after idle detection — - * `mmx text repl` has no launch-time `-i`/prompt flag. + * (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` @@ -72,15 +74,26 @@ export function createMinimaxAdapter(pathOverride?: string): CliAdapter { }, async writeInput(pty: PtyHandle, content: string) { - // mmx's repl is a readline-style prompt (not an Ink bracketed-paste - // widget): it echoes typed characters and submits on Enter. Prefer the - // tmux literal-send + Enter path; fall back to raw write + CR. + // `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(content); + pty.sendText(flattened); await delay(200); pty.sendSpecialKeys('Enter'); } else { - pty.write(content); + pty.write(flattened); await delay(1000); pty.write('\r'); } @@ -89,7 +102,14 @@ export function createMinimaxAdapter(pathOverride?: string): CliAdapter { 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 hints, so inject none (same as mira / riff / mojo). + // routing/@/send hints, and (verified) an inline 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.