openvibers/openvibe.codes Commits
Codes: local adapters for MiniMax Code and Claude Code on another provider (#176)
Changes
@@@ -54,6 +54,8 @@ More: [docs/cli.md](docs/cli.md).| Agent | Adapter | Needs ||---|---|---|| Claude Code | `claude -p --output-format stream-json` | `claude` on PATH |++| Claude Code (another provider) | the same `claude -p --output-format stream-json`, on your own Anthropic-compatible endpoint | `claude` on PATH, `ANTHROPIC_BASE_URL` (the endpoint of your connection) and `ANTHROPIC_AUTH_TOKEN` (its run token); the model id in `ANTHROPIC_MODEL` |++| MiniMax Code | `mcode exec --input - --output-format stream-json` | `mcode` on PATH (your own MiniMax sign-in on this machine) || Codex | `codex exec --json` | `codex` on PATH || OpenCode | `opencode run --format json` | `opencode` on PATH || Command Code | `cmd --print --output-format json` | `cmd` on PATH |@@@ -21,8 +21,8 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your termopenvibe-codes route [--task edit] [--need harness:resume,…] which agent a task would go to, and whyopenvibe-codes run [options] "what to do" run a task ("-" reads it from stdin)--task edit|review|long|resume what kind of task (default edit)-- --agent <id> skip routing: claude-code, codex, opencode, command-code, aider,-- deepseek, openai-compatible++ --agent <id> skip routing: claude-code, claude-other, codex, opencode, command-code,++ aider, minimax-code, deepseek, openai-compatible--model <name> the first agent's model--permission read|edit|full read: look and answer; edit: change files (default);full: anything, unattended@@@ -105,7 +105,7 @@ async function main() {if (cmd === 'agents') {const list = harness.agents();if (o.json) { out(JSON.stringify(list, null, 2)); return 0; }-- for (const a of list) out(`${a.available ? '✔' : '·'} ${a.id.padEnd(18)} ${a.name.padEnd(16)} ${a.available ? dim(a.where || '') : dim(a.reason)}`);++ for (const a of list) out(`${a.available ? '✔' : '·'} ${a.id.padEnd(18)} ${a.name.padEnd(16)} ${a.available ? dim([a.where, a.version].filter(Boolean).join(' · ')) : dim(a.reason)}`);return 0;}if (cmd === 'logins') {@@@ -13,8 +13,10 @@* full anything the agent can do, unattended (its own guardrails only) — for a sandbox or a throwaway checkout* Nothing here pushes, commits or opens a pull request: that is the person's (or a later Improve OpenVibe step's) call.*/++const { execFileSync } = require('child_process');const ev = require('../events');const { spawnAgent, exitReason } = require('../process');++const { effortFor } = require('../../engine/adapters/base');/** A parser that reads JSON lines, hands each object to onEvent, and ends the run with exactly one result. */function jsonLines(agent, onEvent, onEnd) {@@@ -33,6 +35,23 @@ function jsonLines(agent, onEvent, onEnd) {};}++/**++ * What a CLI says about itself: its version, asked of the binary itself (never read from a file of ours, and never++ * a key). A CLI that cannot answer says nothing: the run says why instead.++ *++ * @param {string} bin the executable++ * @param {object} [env] the environment it runs in (PATH)++ * @param {string[]} [args] the arguments that ask++ * @returns {string|null} the version, one line, cut short++ */++function versionOf(bin, env = process.env, args = ['--version']) {++ try {++ const out = execFileSync(bin, args, { env: { ...process.env, ...env }, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });++ const line = String(out || '').split('\n').map((s) => s.trim()).find(Boolean) || '';++ return line ? line.slice(0, 120) : null;++ } catch { return null; }++}++// ── Claude Code: `claude -p --output-format stream-json` already prints the shared events ──const claudeCode = {id: 'claude-code', name: 'Claude Code', kind: 'cli', bin: 'claude',@@@ -215,4 +234,121 @@ const aider = {},};--module.exports = { claudeCode, codex, opencode, commandCode, aider, jsonLines };++++// ── MiniMax Code: `mcode exec --input - --output-format stream-json` (the portal's mm-run.sh) ──++// The argv is the portal's own: the brief rides in on stdin (--input -), the rules would ride in the appended system++// prompt, and the permission is one of mcode's own policies (smart, full, off: "ask" needs the TUI, which a job has++// not). The key is the person's own MiniMax sign-in on this machine; nothing of ours is read, and nothing is written.++const MM_PERMISSION = { read: 'off', edit: 'smart', full: 'full' };++++const minimaxCode = {++ id: 'minimax-code', name: 'MiniMax Code', kind: 'cli', bin: 'mcode',++ /** The version this machine has (asked of the binary itself, never read from a file of ours). */++ version: (env) => versionOf('mcode', env),++ start(t, ctx = {}) {++ const args = ['exec', '--input', '-', '--output-format', 'stream-json', '--prompt-mode', 'coding',++ '--permission', MM_PERMISSION[t.permission] || 'smart', '--cwd', t.cwd || process.cwd(),++ '--timeout', `${Math.max(1, Math.ceil((t.timeoutMs || 45 * 60_000) / 60_000))}m`,++ '--max-steps', String(t.maxTurns || 150)];++ if (t.model) args.push('--model', t.model);++ const effort = effortFor('minimax-code', t.effort);++ if (effort) args.push('--effort', effort);++ if (t.resume) args.push('--session', String(t.resume));++ // Programmatic Tool Calling: the model batches its tool calls in one round trip (the portal's default).++ args.push('--ptc');++ const startedAt = Date.now();++ const parser = jsonLines(minimaxCode, (o, emit, st) => {++ st.startedAt = startedAt;++ if (o.sessionId) st.sessionId = o.sessionId;++ const it = o.item || {};++ const initOnce = () => { if (!st.inited) { st.inited = true; emit(ev.init(st.sessionId, 'minimax-code', t.model)); } };++ if (o.type === 'item.started' && (it.type === 'agent_message' || it.type === 'reasoning')) initOnce();++ else if (o.type === 'item.completed' && it.type === 'agent_message') {++ initOnce();++ const text = String(it.content || it.contentDelta || it.text || '');++ if (text.trim()) { st.lastText = text; emit(ev.text(text, t.model)); }++ } else if (o.type === 'item.completed' && (it.type === 'tool_use' || it.type === 'tool_call')) {++ initOnce();++ emit(ev.toolUse(it.id || it.toolCallId || it.callId || '', it.name || it.toolName || 'tool', it.input || it.arguments || {}, t.model));++ } else if (o.type === 'item.completed' && it.type === 'tool_result') {++ emit(ev.toolResult(it.toolCallId || it.callId || it.id || '', String(it.output || it.content || it.result || ''), !!(it.isError || it.status === 'failed')));++ } else if (o.type === 'turn.failed') {++ st.failed = String((o.error && (o.error.message || o.error)) || 'turn failed');++ emit(ev.error(st.failed));++ } else if (o.type === 'exec.completed') {++ initOnce();++ const r = o.result || {};++ const answer = String(r.answer || r.output || '');++ if (answer) { st.answer = answer; st.lastText = answer; }++ if (r.status) st.status = String(r.status);++ } else if (o.type === 'result') {++ initOnce();++ st.answer = String(o.result || '') || st.answer;++ }++ }, (info, emit, st) => {++ if (!st.sessionId && !st.lastText && !st.answer) return; // nothing ran: the generic exit reason says why++ const text = String(st.answer || st.lastText || '');++ const bad = st.status && st.status !== 'succeeded';++ const ok = !st.failed && !bad && info.code === 0 && !!text;++ const why = st.failed || (bad ? `MiniMax Code stopped with the status ${st.status}` : null) || exitReason(info, 'MiniMax Code');++ emit(ev.result({ ok, text: ok ? text : why, sessionId: st.sessionId, model: t.model, startedAt }));++ });++ return spawnAgent({ cmd: this.bin, args, cwd: t.cwd, env: ctx.env, timeoutMs: t.timeoutMs, signal: t.signal, parser, stdin: t.prompt });++ },++};++++// ── Claude Code on another provider: the same CLI, the cm-style environment (the portal's cm-run.sh) ──++// The endpoint and the model come from the person's own connection (Codes C3): the base URL and the run token travel++// in the environment, and the model id rides in ANTHROPIC_MODEL. No provider is named here: this harness is whatever++// Anthropic-compatible endpoint the person connected, not one of ours.++const ANTHROPIC_ENV = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_CUSTOM_HEADERS'];++++const claudeOther = {++ id: 'claude-other', name: 'Claude Code (another provider)', kind: 'cli', bin: 'claude',++ /** The version this machine has (asked of the binary itself). */++ version: (env) => versionOf('claude', env),++ /** What this harness needs before it can run: the connection's endpoint and its token. */++ check(env) {++ if (!env.ANTHROPIC_BASE_URL) return { available: false, reason: 'set ANTHROPIC_BASE_URL (the endpoint of your connection)' };++ if (!env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_API_KEY) return { available: false, reason: 'set ANTHROPIC_AUTH_TOKEN (the run token of your connection)' };++ return { available: true };++ },++ start(t, ctx = {}) {++ const env = ctx.env || {};++ const mode = { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';++ const args = ['-p', '--output-format', 'stream-json', '--verbose', '--include-partial-messages',++ '--permission-mode', mode, '--exclude-dynamic-system-prompt-sections'];++ if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));++ if (t.resume) args.push('--resume', String(t.resume));++ // Even unattended, a run never pushes: publishing is the person's step.++ args.push('--disallowedTools', 'Bash(git push:*)');++ // The harness's environment (PATH finds the CLI), with the connection's own names on top: the endpoint, the++ // token and the model id. The key travels in the environment, never in argv: the process list of this machine++ // is readable.++ const childEnv = { ...env };++ for (const name of ANTHROPIC_ENV) if (env[name]) childEnv[name] = env[name];++ const model = t.model || env.ANTHROPIC_MODEL || null;++ if (model) childEnv.ANTHROPIC_MODEL = model;++ const effort = effortFor('claude-other', t.effort);++ if (effort) childEnv.CLAUDE_CODE_EFFORT_LEVEL = effort;++ const startedAt = Date.now();++ const parser = jsonLines(claudeOther, (o, emit, st) => {++ if (o.type === 'system' && o.subtype === 'init') {++ st.sessionId = o.session_id; st.model = o.model || model;++ emit(ev.init(o.session_id, 'claude-other', st.model));++ return;++ }++ if (o.type === 'result') { st.sawResult = true; st.answer = String(o.result || ''); emit({ ...o, result: ev.scrub(o.result || '') }); return; }++ if (o.type === 'assistant' || o.type === 'user') emit(o);++ }, (info, emit, st) => {++ if (st.sawResult) return; // Claude Code's own result line already ended the run++ if (!st.sessionId && !st.answer) return; // nothing ran: the generic exit reason says why++ const text = String(st.answer || '');++ const ok = info.code === 0 && !!text;++ emit(ev.result({ ok, text: ok ? text : exitReason(info, 'Claude Code'), sessionId: st.sessionId, model: st.model || model, startedAt }));++ });++ return spawnAgent({ cmd: this.bin, args, cwd: t.cwd, env: childEnv, timeoutMs: t.timeoutMs, signal: t.signal, parser, stdin: t.prompt });++ },++};++++module.exports = { claudeCode, claudeOther, codex, opencode, commandCode, aider, minimaxCode, jsonLines, versionOf };@@@ -57,6 +57,10 @@ function projectProvider(env) {function adaptersFor(env) {return {'claude-code': cli.claudeCode,++ // Claude Code on the person's own endpoint: the same CLI, the connection's environment (Codes C3).++ 'claude-other': cli.claudeOther,++ // MiniMax Code: the person's own MiniMax sign-in on this machine.++ 'minimax-code': cli.minimaxCode,codex: cli.codex,opencode: cli.opencode,'command-code': cli.commandCode,@@@ -82,7 +86,17 @@ function availability(adapter, env) {if (!adapter) return { available: false, reason: 'no adapter for this harness yet' };if (adapter.kind === 'cli') {const where = onPath(adapter.bin, env);-- return where ? { available: true, where } : { available: false, reason: `\`${adapter.bin}\` is not on PATH (install ${adapter.name})` };++ if (!where) return { available: false, reason: `\`${adapter.bin}\` is not on PATH (install ${adapter.name})` };++ // A harness that needs something else besides the binary (Claude Code on another provider needs its endpoint++ // and its token) says so in its own words.++ if (adapter.check) {++ const c = adapter.check(env) || {};++ if (!c.available) return { available: false, reason: c.reason || `${adapter.name} is not set up here` };++ }++ const out = { available: true, where };++ // The version the binary reports about itself, when it answers.++ if (adapter.version) { const v = adapter.version(env); if (v) out.version = v; }++ return out;}if (adapter.id === 'openvibe-ai') return env.OPENVIBE_AI_TOKEN || (env.OPENVIBE_APP_ID && env.OPENVIBE_APP_SECRET) ? { available: true, where: adapter.baseUrl } : { available: false, reason: 'set OPENVIBE_APP_ID and OPENVIBE_APP_SECRET (an app of your project on openvibe.services with ai.app.run)' };if (adapter.id === 'openai-compatible') return env.OPENVIBE_CODES_BASE_URL ? { available: true, where: env.OPENVIBE_CODES_BASE_URL } : { available: false, reason: 'set OPENVIBE_CODES_BASE_URL (and OPENVIBE_CODES_MODEL) to your server' };@@@ -70,7 +70,7 @@ function scrub(s) {* the job's input: everything else in it is ignored.*/const ENV_NAMES = new Set([-- 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_CUSTOM_HEADERS',++ 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_CUSTOM_HEADERS', 'ANTHROPIC_MODEL','OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_CUSTOM_HEADERS','DEEPSEEK_BASE_URL', 'DEEPSEEK_API_KEY','OPENVIBE_AI_URL', 'OPENVIBE_AI_TOKEN', 'OPENVIBE_CODES_BASE_URL', 'OPENVIBE_CODES_API_KEY',@@@ -1,6 +1,6 @@{"name": "openvibe-codes",-- "version": "0.5.0",++ "version": "0.5.1","private": true,"description": "OpenVibe.Codes: the open coding-agent harness. Run a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with routing by capability and cost and hand-offs between agents; plus the site with the agent catalog, a public routing API and the way to improve OpenVibe itself.","main": "harness/index.js",@@@ -188,6 +188,8 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'<p>Install any of them; the harness uses what is there and says why the rest are not.</p>${table(['Agent', 'What it needs'], [[html`<a href="/harnesses#claude-code">Claude Code</a>`, html`<code>claude</code> on PATH, signed in`],++ [html`<a href="/harnesses#claude-other">Claude Code (another provider)</a>`, html`<code>claude</code> on PATH, plus your own Anthropic-compatible endpoint in <code>ANTHROPIC_BASE_URL</code>, its token in <code>ANTHROPIC_AUTH_TOKEN</code> and the model id in <code>ANTHROPIC_MODEL</code>`],++ [html`<a href="/harnesses#minimax-code">MiniMax Code</a>`, html`<code>mcode</code> on PATH, signed in to your own MiniMax account`],[html`<a href="/harnesses#codex">Codex</a>`, html`<code>codex</code> on PATH, signed in`],[html`<a href="/harnesses#opencode">OpenCode</a>`, html`<code>opencode</code> on PATH`],[html`<a href="/harnesses#command-code">Command Code</a>`, html`<code>cmd</code> on PATH`],nnew file mode 100644@@@ -0,0 +1,192 @@++'use strict';++/**++ * The two harnesses the router can select and the local adapters did not know: MiniMax Code (mcode) and Claude Code on++ * another provider (claude-other).++ *++ * What each is given to run with: the argv (the portal's own, from mm-run.sh and cm-run.sh), the environment (the++ * person's own key and endpoint, and never a key in argv), what it says when the binary is not installed, and how a++ * run's output becomes one result.++ *++ * The stand-in CLIs are written here, at run time, and the key-shaped values are built at run time too (a test that++ * spells out a key is a file with a key in it).++ */++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { check, done } = require('./helpers/boot');++const cli = require('../harness/adapters/cli');++const { availability, createHarness } = require('../harness');++const fake = require('./helpers/fakesecrets');++++/** What the stand-in CLIs record: the argv, the stdin and the few environment names that matter here. */++const RECORD_ENV = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL',++ 'ANTHROPIC_CUSTOM_HEADERS', 'CLAUDE_CODE_EFFORT_LEVEL', 'MINIMAX_API_KEY'];++++const FAKE_MCODE = `++const fs = require('fs');++if (process.argv.includes('--version')) { console.log('mcode 9.9.9'); process.exit(0); }++const stdin = fs.readFileSync(0, 'utf8');++const rec = { argv: process.argv.slice(2), stdin, env: {} };++for (const k of ${JSON.stringify(RECORD_ENV)}) rec.env[k] = process.env[k] == null ? null : process.env[k];++fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify(rec));++const out = (o) => console.log(JSON.stringify(o));++out({ schemaVersion: 1, type: 'item.started', sessionId: 'mm-1', item: { type: 'agent_message' } });++if (process.env.FAKE_MMODE === 'fail') {++ out({ schemaVersion: 1, type: 'turn.failed', sessionId: 'mm-1', error: { message: 'the plan ran out' } });++ process.exit(1);++}++out({ schemaVersion: 1, type: 'item.completed', sessionId: 'mm-1', item: { type: 'agent_message', content: 'minimax did it' } });++out({ schemaVersion: 1, type: 'exec.completed', sessionId: 'mm-1', result: { status: 'succeeded', answer: 'minimax did it' } });++`;++++const FAKE_CLAUDE = `++const fs = require('fs');++if (process.argv.includes('--version')) { console.log('2.1.314 (Claude Code)'); process.exit(0); }++const stdin = fs.readFileSync(0, 'utf8');++const rec = { argv: process.argv.slice(2), stdin, env: {} };++for (const k of ${JSON.stringify(RECORD_ENV)}) rec.env[k] = process.env[k] == null ? null : process.env[k];++fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify(rec));++const out = (o) => console.log(JSON.stringify(o));++out({ type: 'system', subtype: 'init', session_id: 'cl-9', model: 'other-model' });++out({ type: 'assistant', message: { model: 'other-model', content: [{ type: 'text', text: 'claude did it' }] } });++out({ type: 'result', subtype: 'success', is_error: false, result: 'claude did it', session_id: 'cl-9', duration_ms: 5, num_turns: 1, total_cost_usd: 0.01 });++`;++++/** A directory with the stand-in CLIs in it, and the record file they write. */++function fakes(names) {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-mm-'));++ const bin = path.join(dir, 'bin');++ fs.mkdirSync(bin);++ const scripts = { mcode: FAKE_MCODE, claude: FAKE_CLAUDE };++ for (const n of names) fs.writeFileSync(path.join(bin, n), `#!${process.execPath}\n'use strict';\n${scripts[n]}\n`, { mode: 0o755 });++ const record = path.join(dir, 'record.json');++ return { dir, bin, record, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };++}++++/** Every event one adapter's run produces. */++async function run(adapter, task, env) {++ const events = [];++ for await (const e of adapter.start(task, { env })) events.push(e);++ return events;++}++++/** The value of an argument in an argv. */++const argOf = (argv, name) => { const i = argv.indexOf(name); return i < 0 ? null : argv[i + 1]; };++++(async () => {++ const f = fakes(['mcode', 'claude']);++++ await check('MiniMax Code: the argv the portal\'s mm-run.sh uses, and the brief on stdin', async () => {++ const events = await run(cli.minimaxCode, { prompt: 'rename the variable', cwd: f.dir, permission: 'edit' },++ { PATH: f.bin, FAKE_RECORD: f.record });++ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));++ assert.ok(rec.argv.includes('exec'), rec.argv.join(' '));++ assert.strictEqual(argOf(rec.argv, '--input'), '-');++ assert.strictEqual(argOf(rec.argv, '--output-format'), 'stream-json');++ assert.strictEqual(argOf(rec.argv, '--prompt-mode'), 'coding');++ assert.strictEqual(argOf(rec.argv, '--permission'), 'smart');++ assert.strictEqual(argOf(rec.argv, '--cwd'), f.dir);++ assert.strictEqual(argOf(rec.argv, '--max-steps'), '150');++ assert.ok(rec.argv.includes('--ptc'), 'the portal batches its tool calls');++ assert.strictEqual(rec.stdin, 'rename the variable', 'the brief rides in on stdin');++ assert.ok(!rec.argv.join(' ').includes('rename the variable'), 'not in argv');++ assert.strictEqual(events.filter((e) => e.type === 'result').length, 1, 'one result');++ });++++ await check('MiniMax Code: the permission level is one of mcode\'s own policies', async () => {++ for (const [level, policy] of [['read', 'off'], ['edit', 'smart'], ['full', 'full']]) {++ await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: level }, { PATH: f.bin, FAKE_RECORD: f.record });++ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));++ assert.strictEqual(argOf(rec.argv, '--permission'), policy, level);++ }++ });++++ await check('MiniMax Code: a resume continues the session, and no key sits in argv', async () => {++ const key = fake.providerKey();++ await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: 'edit', resume: 'mm-9', model: 'MiniMax-M2' },++ { PATH: f.bin, FAKE_RECORD: f.record, MINIMAX_API_KEY: key });++ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));++ assert.strictEqual(argOf(rec.argv, '--session'), 'mm-9');++ assert.strictEqual(argOf(rec.argv, '--model'), 'MiniMax-M2');++ assert.ok(!rec.argv.join(' ').includes(key), 'the key is not in argv');++ assert.strictEqual(rec.env.MINIMAX_API_KEY, key, 'the key is in the environment');++ });++++ await check('MiniMax Code: the run\'s answer is one result, and a failure says why', async () => {++ const ok = await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: 'edit' }, { PATH: f.bin, FAKE_RECORD: f.record });++ const result = ok[ok.length - 1];++ assert.strictEqual(result.type, 'result');++ assert.strictEqual(result.is_error, false, result.result);++ assert.strictEqual(result.result, 'minimax did it');++ assert.strictEqual(result.session_id, 'mm-1');++ const bad = await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: 'edit' }, { PATH: f.bin, FAKE_RECORD: f.record, FAKE_MMODE: 'fail' });++ const failed = bad[bad.length - 1];++ assert.strictEqual(failed.type, 'result');++ assert.strictEqual(failed.is_error, true);++ assert.match(failed.result, /the plan ran out/);++ });++++ await check('Claude Code on another provider: the endpoint and the model come from the connection', async () => {++ const key = fake.providerKey();++ const endpoint = 'https://provider.example/anthropic';++ const events = await run(cli.claudeOther, { prompt: 'fix the test', cwd: f.dir, permission: 'edit' },++ { PATH: f.bin, FAKE_RECORD: f.record, ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: key, ANTHROPIC_MODEL: 'provider-model-x' });++ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));++ assert.strictEqual(rec.env.ANTHROPIC_BASE_URL, endpoint, 'the endpoint of the connection');++ assert.strictEqual(rec.env.ANTHROPIC_AUTH_TOKEN, key, 'the key of the connection');++ assert.strictEqual(rec.env.ANTHROPIC_MODEL, 'provider-model-x', 'the model id of the connection');++ assert.ok(!rec.argv.join(' ').includes(key), 'the key is never in argv');++ assert.ok(!rec.argv.join(' ').toLowerCase().includes('minimax'), 'no provider is hard-coded');++ assert.strictEqual(argOf(rec.argv, '--output-format'), 'stream-json');++ assert.strictEqual(argOf(rec.argv, '--permission-mode'), 'acceptEdits');++ assert.strictEqual(rec.stdin, 'fix the test', 'the brief rides in on stdin');++ const result = events[events.length - 1];++ assert.strictEqual(result.type, 'result');++ assert.strictEqual(result.is_error, false, result.result);++ assert.strictEqual(result.result, 'claude did it');++ });++++ await check('Claude Code on another provider: a resume continues the session', async () => {++ await run(cli.claudeOther, { prompt: 'x', cwd: f.dir, permission: 'edit', resume: 'cl-9' },++ { PATH: f.bin, FAKE_RECORD: f.record, ANTHROPIC_BASE_URL: 'https://provider.example/anthropic', ANTHROPIC_AUTH_TOKEN: fake.providerKey() });++ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));++ assert.strictEqual(argOf(rec.argv, '--resume'), 'cl-9');++ });++++ await check('detection: the binary that is not there, and the one that is', () => {++ const missing = availability(cli.minimaxCode, { PATH: '/nonexistent' });++ assert.strictEqual(missing.available, false);++ assert.match(missing.reason, /not on PATH/);++ const there = availability(cli.minimaxCode, { PATH: f.bin });++ assert.strictEqual(there.available, true, there.reason);++ assert.match(there.version, /mcode/, 'the version it reports about itself');++ });++++ await check('detection: Claude Code on another provider needs its connection', () => {++ const bare = availability(cli.claudeOther, { PATH: f.bin });++ assert.strictEqual(bare.available, false);++ assert.match(bare.reason, /ANTHROPIC_BASE_URL/);++ const noToken = availability(cli.claudeOther, { PATH: f.bin, ANTHROPIC_BASE_URL: 'https://provider.example/anthropic' });++ assert.strictEqual(noToken.available, false);++ assert.match(noToken.reason, /ANTHROPIC_AUTH_TOKEN/);++ const ready = availability(cli.claudeOther, { PATH: f.bin, ANTHROPIC_BASE_URL: 'https://provider.example/anthropic', ANTHROPIC_AUTH_TOKEN: fake.providerKey() });++ assert.strictEqual(ready.available, true, ready.reason);++ assert.match(ready.version, /Claude Code/);++ });++++ await check('the harness knows both harnesses, and says why they are down', () => {++ const harness = createHarness({ env: { PATH: f.bin } });++ const rows = harness.agents();++ const mm = rows.find((r) => r.id === 'minimax-code');++ const other = rows.find((r) => r.id === 'claude-other');++ assert.ok(mm && other, 'both are in the catalogue');++ assert.strictEqual(mm.adapter, 'cli');++ assert.strictEqual(mm.available, true, mm.reason);++ assert.strictEqual(other.available, false);++ assert.match(other.reason, /ANTHROPIC_BASE_URL/);++ });++++ f.cleanup();++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -54,6 +54,8 @@ More: [docs/cli.md](docs/cli.md).
@@ -54,6 +54,8 @@ More: [docs/cli.md](docs/cli.md).
| Agent | Adapter | Needs |
| Agent | Adapter | Needs |
|---|---|---|
|---|---|---|
| Claude Code | `claude -p --output-format stream-json` | `claude` on PATH |
| Claude Code | `claude -p --output-format stream-json` | `claude` on PATH |
+| Claude Code (another provider) | the same `claude -p --output-format stream-json`, on your own Anthropic-compatible endpoint | `claude` on PATH, `ANTHROPIC_BASE_URL` (the endpoint of your connection) and `ANTHROPIC_AUTH_TOKEN` (its run token); the model id in `ANTHROPIC_MODEL` |
+| MiniMax Code | `mcode exec --input - --output-format stream-json` | `mcode` on PATH (your own MiniMax sign-in on this machine) |
| Codex | `codex exec --json` | `codex` on PATH |
| Codex | `codex exec --json` | `codex` on PATH |
| OpenCode | `opencode run --format json` | `opencode` on PATH |
| OpenCode | `opencode run --format json` | `opencode` on PATH |
| Command Code | `cmd --print --output-format json` | `cmd` on PATH |
| Command Code | `cmd --print --output-format json` | `cmd` on PATH |
@@ -21,8 +21,8 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
@@ -21,8 +21,8 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
openvibe-codes route [--task edit] [--need harness:resume,…] which agent a task would go to, and why
openvibe-codes route [--task edit] [--need harness:resume,…] which agent a task would go to, and why
openvibe-codes run [options] "what to do" run a task ("-" reads it from stdin)
openvibe-codes run [options] "what to do" run a task ("-" reads it from stdin)
--task edit|review|long|resume what kind of task (default edit)
--task edit|review|long|resume what kind of task (default edit)
- --agent <id> skip routing: claude-code, codex, opencode, command-code, aider,
- deepseek, openai-compatible
+ --agent <id> skip routing: claude-code, claude-other, codex, opencode, command-code,
+ aider, minimax-code, deepseek, openai-compatible
--model <name> the first agent's model
--model <name> the first agent's model
--permission read|edit|full read: look and answer; edit: change files (default);
--permission read|edit|full read: look and answer; edit: change files (default);
full: anything, unattended
full: anything, unattended
@@ -105,7 +105,7 @@ async function main() {
@@ -105,7 +105,7 @@ async function main() {
if (cmd === 'agents') {
if (cmd === 'agents') {
const list = harness.agents();
const list = harness.agents();
if (o.json) { out(JSON.stringify(list, null, 2)); return 0; }
if (o.json) { out(JSON.stringify(list, null, 2)); return 0; }
- for (const a of list) out(`${a.available ? '✔' : '·'} ${a.id.padEnd(18)} ${a.name.padEnd(16)} ${a.available ? dim(a.where || '') : dim(a.reason)}`);
+ for (const a of list) out(`${a.available ? '✔' : '·'} ${a.id.padEnd(18)} ${a.name.padEnd(16)} ${a.available ? dim([a.where, a.version].filter(Boolean).join(' · ')) : dim(a.reason)}`);
return 0;
return 0;
}
}
if (cmd === 'logins') {
if (cmd === 'logins') {
@@ -13,8 +13,10 @@
@@ -13,8 +13,10 @@
* full anything the agent can do, unattended (its own guardrails only) — for a sandbox or a throwaway checkout
* full anything the agent can do, unattended (its own guardrails only) — for a sandbox or a throwaway checkout
* Nothing here pushes, commits or opens a pull request: that is the person's (or a later Improve OpenVibe step's) call.
* Nothing here pushes, commits or opens a pull request: that is the person's (or a later Improve OpenVibe step's) call.
*/
*/
+const { execFileSync } = require('child_process');
const ev = require('../events');
const ev = require('../events');
const { spawnAgent, exitReason } = require('../process');
const { spawnAgent, exitReason } = require('../process');
+const { effortFor } = require('../../engine/adapters/base');
/** A parser that reads JSON lines, hands each object to onEvent, and ends the run with exactly one result. */
/** A parser that reads JSON lines, hands each object to onEvent, and ends the run with exactly one result. */
function jsonLines(agent, onEvent, onEnd) {
function jsonLines(agent, onEvent, onEnd) {
@@ -33,6 +35,23 @@ function jsonLines(agent, onEvent, onEnd) {
@@ -33,6 +35,23 @@ function jsonLines(agent, onEvent, onEnd) {
};
};
}
}
+/**
+ * What a CLI says about itself: its version, asked of the binary itself (never read from a file of ours, and never
+ * a key). A CLI that cannot answer says nothing: the run says why instead.
+ *
+ * @param {string} bin the executable
+ * @param {object} [env] the environment it runs in (PATH)
+ * @param {string[]} [args] the arguments that ask
+ * @returns {string|null} the version, one line, cut short
+ */
+function versionOf(bin, env = process.env, args = ['--version']) {
+ try {
+ const out = execFileSync(bin, args, { env: { ...process.env, ...env }, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
+ const line = String(out || '').split('\n').map((s) => s.trim()).find(Boolean) || '';
+ return line ? line.slice(0, 120) : null;
+ } catch { return null; }
+}
+
// ── Claude Code: `claude -p --output-format stream-json` already prints the shared events ──
// ── Claude Code: `claude -p --output-format stream-json` already prints the shared events ──
const claudeCode = {
const claudeCode = {
id: 'claude-code', name: 'Claude Code', kind: 'cli', bin: 'claude',
id: 'claude-code', name: 'Claude Code', kind: 'cli', bin: 'claude',
@@ -215,4 +234,121 @@ const aider = {
@@ -215,4 +234,121 @@ const aider = {
},
},
};
};
-module.exports = { claudeCode, codex, opencode, commandCode, aider, jsonLines };
+
+// ── MiniMax Code: `mcode exec --input - --output-format stream-json` (the portal's mm-run.sh) ──
+// The argv is the portal's own: the brief rides in on stdin (--input -), the rules would ride in the appended system
+// prompt, and the permission is one of mcode's own policies (smart, full, off: "ask" needs the TUI, which a job has
+// not). The key is the person's own MiniMax sign-in on this machine; nothing of ours is read, and nothing is written.
+const MM_PERMISSION = { read: 'off', edit: 'smart', full: 'full' };
+
+const minimaxCode = {
+ id: 'minimax-code', name: 'MiniMax Code', kind: 'cli', bin: 'mcode',
+ /** The version this machine has (asked of the binary itself, never read from a file of ours). */
+ version: (env) => versionOf('mcode', env),
+ start(t, ctx = {}) {
+ const args = ['exec', '--input', '-', '--output-format', 'stream-json', '--prompt-mode', 'coding',
+ '--permission', MM_PERMISSION[t.permission] || 'smart', '--cwd', t.cwd || process.cwd(),
+ '--timeout', `${Math.max(1, Math.ceil((t.timeoutMs || 45 * 60_000) / 60_000))}m`,
+ '--max-steps', String(t.maxTurns || 150)];
+ if (t.model) args.push('--model', t.model);
+ const effort = effortFor('minimax-code', t.effort);
+ if (effort) args.push('--effort', effort);
+ if (t.resume) args.push('--session', String(t.resume));
+ // Programmatic Tool Calling: the model batches its tool calls in one round trip (the portal's default).
+ args.push('--ptc');
+ const startedAt = Date.now();
+ const parser = jsonLines(minimaxCode, (o, emit, st) => {
+ st.startedAt = startedAt;
+ if (o.sessionId) st.sessionId = o.sessionId;
+ const it = o.item || {};
+ const initOnce = () => { if (!st.inited) { st.inited = true; emit(ev.init(st.sessionId, 'minimax-code', t.model)); } };
+ if (o.type === 'item.started' && (it.type === 'agent_message' || it.type === 'reasoning')) initOnce();
+ else if (o.type === 'item.completed' && it.type === 'agent_message') {
+ initOnce();
+ const text = String(it.content || it.contentDelta || it.text || '');
+ if (text.trim()) { st.lastText = text; emit(ev.text(text, t.model)); }
+ } else if (o.type === 'item.completed' && (it.type === 'tool_use' || it.type === 'tool_call')) {
+ initOnce();
+ emit(ev.toolUse(it.id || it.toolCallId || it.callId || '', it.name || it.toolName || 'tool', it.input || it.arguments || {}, t.model));
+ } else if (o.type === 'item.completed' && it.type === 'tool_result') {
+ emit(ev.toolResult(it.toolCallId || it.callId || it.id || '', String(it.output || it.content || it.result || ''), !!(it.isError || it.status === 'failed')));
+ } else if (o.type === 'turn.failed') {
+ st.failed = String((o.error && (o.error.message || o.error)) || 'turn failed');
+ emit(ev.error(st.failed));
+ } else if (o.type === 'exec.completed') {
+ initOnce();
+ const r = o.result || {};
+ const answer = String(r.answer || r.output || '');
+ if (answer) { st.answer = answer; st.lastText = answer; }
+ if (r.status) st.status = String(r.status);
+ } else if (o.type === 'result') {
+ initOnce();
+ st.answer = String(o.result || '') || st.answer;
+ }
+ }, (info, emit, st) => {
+ if (!st.sessionId && !st.lastText && !st.answer) return; // nothing ran: the generic exit reason says why
+ const text = String(st.answer || st.lastText || '');
+ const bad = st.status && st.status !== 'succeeded';
+ const ok = !st.failed && !bad && info.code === 0 && !!text;
+ const why = st.failed || (bad ? `MiniMax Code stopped with the status ${st.status}` : null) || exitReason(info, 'MiniMax Code');
+ emit(ev.result({ ok, text: ok ? text : why, sessionId: st.sessionId, model: t.model, startedAt }));
+ });
+ return spawnAgent({ cmd: this.bin, args, cwd: t.cwd, env: ctx.env, timeoutMs: t.timeoutMs, signal: t.signal, parser, stdin: t.prompt });
+ },
+};
+
+// ── Claude Code on another provider: the same CLI, the cm-style environment (the portal's cm-run.sh) ──
+// The endpoint and the model come from the person's own connection (Codes C3): the base URL and the run token travel
+// in the environment, and the model id rides in ANTHROPIC_MODEL. No provider is named here: this harness is whatever
+// Anthropic-compatible endpoint the person connected, not one of ours.
+const ANTHROPIC_ENV = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_CUSTOM_HEADERS'];
+
+const claudeOther = {
+ id: 'claude-other', name: 'Claude Code (another provider)', kind: 'cli', bin: 'claude',
+ /** The version this machine has (asked of the binary itself). */
+ version: (env) => versionOf('claude', env),
+ /** What this harness needs before it can run: the connection's endpoint and its token. */
+ check(env) {
+ if (!env.ANTHROPIC_BASE_URL) return { available: false, reason: 'set ANTHROPIC_BASE_URL (the endpoint of your connection)' };
+ if (!env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_API_KEY) return { available: false, reason: 'set ANTHROPIC_AUTH_TOKEN (the run token of your connection)' };
+ return { available: true };
+ },
+ start(t, ctx = {}) {
+ const env = ctx.env || {};
+ const mode = { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';
+ const args = ['-p', '--output-format', 'stream-json', '--verbose', '--include-partial-messages',
+ '--permission-mode', mode, '--exclude-dynamic-system-prompt-sections'];
+ if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
+ if (t.resume) args.push('--resume', String(t.resume));
+ // Even unattended, a run never pushes: publishing is the person's step.
+ args.push('--disallowedTools', 'Bash(git push:*)');
+ // The harness's environment (PATH finds the CLI), with the connection's own names on top: the endpoint, the
+ // token and the model id. The key travels in the environment, never in argv: the process list of this machine
+ // is readable.
+ const childEnv = { ...env };
+ for (const name of ANTHROPIC_ENV) if (env[name]) childEnv[name] = env[name];
+ const model = t.model || env.ANTHROPIC_MODEL || null;
+ if (model) childEnv.ANTHROPIC_MODEL = model;
+ const effort = effortFor('claude-other', t.effort);
+ if (effort) childEnv.CLAUDE_CODE_EFFORT_LEVEL = effort;
+ const startedAt = Date.now();
+ const parser = jsonLines(claudeOther, (o, emit, st) => {
+ if (o.type === 'system' && o.subtype === 'init') {
+ st.sessionId = o.session_id; st.model = o.model || model;
+ emit(ev.init(o.session_id, 'claude-other', st.model));
+ return;
+ }
+ if (o.type === 'result') { st.sawResult = true; st.answer = String(o.result || ''); emit({ ...o, result: ev.scrub(o.result || '') }); return; }
+ if (o.type === 'assistant' || o.type === 'user') emit(o);
+ }, (info, emit, st) => {
+ if (st.sawResult) return; // Claude Code's own result line already ended the run
+ if (!st.sessionId && !st.answer) return; // nothing ran: the generic exit reason says why
+ const text = String(st.answer || '');
+ const ok = info.code === 0 && !!text;
+ emit(ev.result({ ok, text: ok ? text : exitReason(info, 'Claude Code'), sessionId: st.sessionId, model: st.model || model, startedAt }));
+ });
+ return spawnAgent({ cmd: this.bin, args, cwd: t.cwd, env: childEnv, timeoutMs: t.timeoutMs, signal: t.signal, parser, stdin: t.prompt });
+ },
+};
+
+module.exports = { claudeCode, claudeOther, codex, opencode, commandCode, aider, minimaxCode, jsonLines, versionOf };
@@ -57,6 +57,10 @@ function projectProvider(env) {
@@ -57,6 +57,10 @@ function projectProvider(env) {
function adaptersFor(env) {
function adaptersFor(env) {
return {
return {
'claude-code': cli.claudeCode,
'claude-code': cli.claudeCode,
+ // Claude Code on the person's own endpoint: the same CLI, the connection's environment (Codes C3).
+ 'claude-other': cli.claudeOther,
+ // MiniMax Code: the person's own MiniMax sign-in on this machine.
+ 'minimax-code': cli.minimaxCode,
codex: cli.codex,
codex: cli.codex,
opencode: cli.opencode,
opencode: cli.opencode,
'command-code': cli.commandCode,
'command-code': cli.commandCode,
@@ -82,7 +86,17 @@ function availability(adapter, env) {
@@ -82,7 +86,17 @@ function availability(adapter, env) {
if (!adapter) return { available: false, reason: 'no adapter for this harness yet' };
if (!adapter) return { available: false, reason: 'no adapter for this harness yet' };
if (adapter.kind === 'cli') {
if (adapter.kind === 'cli') {
const where = onPath(adapter.bin, env);
const where = onPath(adapter.bin, env);
- return where ? { available: true, where } : { available: false, reason: `\`${adapter.bin}\` is not on PATH (install ${adapter.name})` };
+ if (!where) return { available: false, reason: `\`${adapter.bin}\` is not on PATH (install ${adapter.name})` };
+ // A harness that needs something else besides the binary (Claude Code on another provider needs its endpoint
+ // and its token) says so in its own words.
+ if (adapter.check) {
+ const c = adapter.check(env) || {};
+ if (!c.available) return { available: false, reason: c.reason || `${adapter.name} is not set up here` };
+ }
+ const out = { available: true, where };
+ // The version the binary reports about itself, when it answers.
+ if (adapter.version) { const v = adapter.version(env); if (v) out.version = v; }
+ return out;
}
}
if (adapter.id === 'openvibe-ai') return env.OPENVIBE_AI_TOKEN || (env.OPENVIBE_APP_ID && env.OPENVIBE_APP_SECRET) ? { available: true, where: adapter.baseUrl } : { available: false, reason: 'set OPENVIBE_APP_ID and OPENVIBE_APP_SECRET (an app of your project on openvibe.services with ai.app.run)' };
if (adapter.id === 'openvibe-ai') return env.OPENVIBE_AI_TOKEN || (env.OPENVIBE_APP_ID && env.OPENVIBE_APP_SECRET) ? { available: true, where: adapter.baseUrl } : { available: false, reason: 'set OPENVIBE_APP_ID and OPENVIBE_APP_SECRET (an app of your project on openvibe.services with ai.app.run)' };
if (adapter.id === 'openai-compatible') return env.OPENVIBE_CODES_BASE_URL ? { available: true, where: env.OPENVIBE_CODES_BASE_URL } : { available: false, reason: 'set OPENVIBE_CODES_BASE_URL (and OPENVIBE_CODES_MODEL) to your server' };
if (adapter.id === 'openai-compatible') return env.OPENVIBE_CODES_BASE_URL ? { available: true, where: env.OPENVIBE_CODES_BASE_URL } : { available: false, reason: 'set OPENVIBE_CODES_BASE_URL (and OPENVIBE_CODES_MODEL) to your server' };
@@ -70,7 +70,7 @@ function scrub(s) {
@@ -70,7 +70,7 @@ function scrub(s) {
* the job's input: everything else in it is ignored.
* the job's input: everything else in it is ignored.
*/
*/
const ENV_NAMES = new Set([
const ENV_NAMES = new Set([
- 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_CUSTOM_HEADERS',
+ 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_CUSTOM_HEADERS', 'ANTHROPIC_MODEL',
'OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_CUSTOM_HEADERS',
'OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_CUSTOM_HEADERS',
'DEEPSEEK_BASE_URL', 'DEEPSEEK_API_KEY',
'DEEPSEEK_BASE_URL', 'DEEPSEEK_API_KEY',
'OPENVIBE_AI_URL', 'OPENVIBE_AI_TOKEN', 'OPENVIBE_CODES_BASE_URL', 'OPENVIBE_CODES_API_KEY',
'OPENVIBE_AI_URL', 'OPENVIBE_AI_TOKEN', 'OPENVIBE_CODES_BASE_URL', 'OPENVIBE_CODES_API_KEY',
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-codes",
"name": "openvibe-codes",
- "version": "0.5.0",
+ "version": "0.5.1",
"private": true,
"private": true,
"description": "OpenVibe.Codes: the open coding-agent harness. Run a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with routing by capability and cost and hand-offs between agents; plus the site with the agent catalog, a public routing API and the way to improve OpenVibe itself.",
"description": "OpenVibe.Codes: the open coding-agent harness. Run a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with routing by capability and cost and hand-offs between agents; plus the site with the agent catalog, a public routing API and the way to improve OpenVibe itself.",
"main": "harness/index.js",
"main": "harness/index.js",
@@ -188,6 +188,8 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'
@@ -188,6 +188,8 @@ for await (const event of harness.run({ prompt: 'fix the failing test', cwd: '.'
<p>Install any of them; the harness uses what is there and says why the rest are not.</p>
<p>Install any of them; the harness uses what is there and says why the rest are not.</p>
${table(['Agent', 'What it needs'], [
${table(['Agent', 'What it needs'], [
[html`<a href="/harnesses#claude-code">Claude Code</a>`, html`<code>claude</code> on PATH, signed in`],
[html`<a href="/harnesses#claude-code">Claude Code</a>`, html`<code>claude</code> on PATH, signed in`],
+ [html`<a href="/harnesses#claude-other">Claude Code (another provider)</a>`, html`<code>claude</code> on PATH, plus your own Anthropic-compatible endpoint in <code>ANTHROPIC_BASE_URL</code>, its token in <code>ANTHROPIC_AUTH_TOKEN</code> and the model id in <code>ANTHROPIC_MODEL</code>`],
+ [html`<a href="/harnesses#minimax-code">MiniMax Code</a>`, html`<code>mcode</code> on PATH, signed in to your own MiniMax account`],
[html`<a href="/harnesses#codex">Codex</a>`, html`<code>codex</code> on PATH, signed in`],
[html`<a href="/harnesses#codex">Codex</a>`, html`<code>codex</code> on PATH, signed in`],
[html`<a href="/harnesses#opencode">OpenCode</a>`, html`<code>opencode</code> on PATH`],
[html`<a href="/harnesses#opencode">OpenCode</a>`, html`<code>opencode</code> on PATH`],
[html`<a href="/harnesses#command-code">Command Code</a>`, html`<code>cmd</code> on PATH`],
[html`<a href="/harnesses#command-code">Command Code</a>`, html`<code>cmd</code> on PATH`],
new file mode 100644
new file mode 100644
@@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
+'use strict';
+/**
+ * The two harnesses the router can select and the local adapters did not know: MiniMax Code (mcode) and Claude Code on
+ * another provider (claude-other).
+ *
+ * What each is given to run with: the argv (the portal's own, from mm-run.sh and cm-run.sh), the environment (the
+ * person's own key and endpoint, and never a key in argv), what it says when the binary is not installed, and how a
+ * run's output becomes one result.
+ *
+ * The stand-in CLIs are written here, at run time, and the key-shaped values are built at run time too (a test that
+ * spells out a key is a file with a key in it).
+ */
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { check, done } = require('./helpers/boot');
+const cli = require('../harness/adapters/cli');
+const { availability, createHarness } = require('../harness');
+const fake = require('./helpers/fakesecrets');
+
+/** What the stand-in CLIs record: the argv, the stdin and the few environment names that matter here. */
+const RECORD_ENV = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL',
+ 'ANTHROPIC_CUSTOM_HEADERS', 'CLAUDE_CODE_EFFORT_LEVEL', 'MINIMAX_API_KEY'];
+
+const FAKE_MCODE = `
+const fs = require('fs');
+if (process.argv.includes('--version')) { console.log('mcode 9.9.9'); process.exit(0); }
+const stdin = fs.readFileSync(0, 'utf8');
+const rec = { argv: process.argv.slice(2), stdin, env: {} };
+for (const k of ${JSON.stringify(RECORD_ENV)}) rec.env[k] = process.env[k] == null ? null : process.env[k];
+fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify(rec));
+const out = (o) => console.log(JSON.stringify(o));
+out({ schemaVersion: 1, type: 'item.started', sessionId: 'mm-1', item: { type: 'agent_message' } });
+if (process.env.FAKE_MMODE === 'fail') {
+ out({ schemaVersion: 1, type: 'turn.failed', sessionId: 'mm-1', error: { message: 'the plan ran out' } });
+ process.exit(1);
+}
+out({ schemaVersion: 1, type: 'item.completed', sessionId: 'mm-1', item: { type: 'agent_message', content: 'minimax did it' } });
+out({ schemaVersion: 1, type: 'exec.completed', sessionId: 'mm-1', result: { status: 'succeeded', answer: 'minimax did it' } });
+`;
+
+const FAKE_CLAUDE = `
+const fs = require('fs');
+if (process.argv.includes('--version')) { console.log('2.1.314 (Claude Code)'); process.exit(0); }
+const stdin = fs.readFileSync(0, 'utf8');
+const rec = { argv: process.argv.slice(2), stdin, env: {} };
+for (const k of ${JSON.stringify(RECORD_ENV)}) rec.env[k] = process.env[k] == null ? null : process.env[k];
+fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify(rec));
+const out = (o) => console.log(JSON.stringify(o));
+out({ type: 'system', subtype: 'init', session_id: 'cl-9', model: 'other-model' });
+out({ type: 'assistant', message: { model: 'other-model', content: [{ type: 'text', text: 'claude did it' }] } });
+out({ type: 'result', subtype: 'success', is_error: false, result: 'claude did it', session_id: 'cl-9', duration_ms: 5, num_turns: 1, total_cost_usd: 0.01 });
+`;
+
+/** A directory with the stand-in CLIs in it, and the record file they write. */
+function fakes(names) {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-mm-'));
+ const bin = path.join(dir, 'bin');
+ fs.mkdirSync(bin);
+ const scripts = { mcode: FAKE_MCODE, claude: FAKE_CLAUDE };
+ for (const n of names) fs.writeFileSync(path.join(bin, n), `#!${process.execPath}\n'use strict';\n${scripts[n]}\n`, { mode: 0o755 });
+ const record = path.join(dir, 'record.json');
+ return { dir, bin, record, cleanup: () => fs.rmSync(dir, { recursive: true, force: true }) };
+}
+
+/** Every event one adapter's run produces. */
+async function run(adapter, task, env) {
+ const events = [];
+ for await (const e of adapter.start(task, { env })) events.push(e);
+ return events;
+}
+
+/** The value of an argument in an argv. */
+const argOf = (argv, name) => { const i = argv.indexOf(name); return i < 0 ? null : argv[i + 1]; };
+
+(async () => {
+ const f = fakes(['mcode', 'claude']);
+
+ await check('MiniMax Code: the argv the portal\'s mm-run.sh uses, and the brief on stdin', async () => {
+ const events = await run(cli.minimaxCode, { prompt: 'rename the variable', cwd: f.dir, permission: 'edit' },
+ { PATH: f.bin, FAKE_RECORD: f.record });
+ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));
+ assert.ok(rec.argv.includes('exec'), rec.argv.join(' '));
+ assert.strictEqual(argOf(rec.argv, '--input'), '-');
+ assert.strictEqual(argOf(rec.argv, '--output-format'), 'stream-json');
+ assert.strictEqual(argOf(rec.argv, '--prompt-mode'), 'coding');
+ assert.strictEqual(argOf(rec.argv, '--permission'), 'smart');
+ assert.strictEqual(argOf(rec.argv, '--cwd'), f.dir);
+ assert.strictEqual(argOf(rec.argv, '--max-steps'), '150');
+ assert.ok(rec.argv.includes('--ptc'), 'the portal batches its tool calls');
+ assert.strictEqual(rec.stdin, 'rename the variable', 'the brief rides in on stdin');
+ assert.ok(!rec.argv.join(' ').includes('rename the variable'), 'not in argv');
+ assert.strictEqual(events.filter((e) => e.type === 'result').length, 1, 'one result');
+ });
+
+ await check('MiniMax Code: the permission level is one of mcode\'s own policies', async () => {
+ for (const [level, policy] of [['read', 'off'], ['edit', 'smart'], ['full', 'full']]) {
+ await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: level }, { PATH: f.bin, FAKE_RECORD: f.record });
+ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));
+ assert.strictEqual(argOf(rec.argv, '--permission'), policy, level);
+ }
+ });
+
+ await check('MiniMax Code: a resume continues the session, and no key sits in argv', async () => {
+ const key = fake.providerKey();
+ await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: 'edit', resume: 'mm-9', model: 'MiniMax-M2' },
+ { PATH: f.bin, FAKE_RECORD: f.record, MINIMAX_API_KEY: key });
+ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));
+ assert.strictEqual(argOf(rec.argv, '--session'), 'mm-9');
+ assert.strictEqual(argOf(rec.argv, '--model'), 'MiniMax-M2');
+ assert.ok(!rec.argv.join(' ').includes(key), 'the key is not in argv');
+ assert.strictEqual(rec.env.MINIMAX_API_KEY, key, 'the key is in the environment');
+ });
+
+ await check('MiniMax Code: the run\'s answer is one result, and a failure says why', async () => {
+ const ok = await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: 'edit' }, { PATH: f.bin, FAKE_RECORD: f.record });
+ const result = ok[ok.length - 1];
+ assert.strictEqual(result.type, 'result');
+ assert.strictEqual(result.is_error, false, result.result);
+ assert.strictEqual(result.result, 'minimax did it');
+ assert.strictEqual(result.session_id, 'mm-1');
+ const bad = await run(cli.minimaxCode, { prompt: 'x', cwd: f.dir, permission: 'edit' }, { PATH: f.bin, FAKE_RECORD: f.record, FAKE_MMODE: 'fail' });
+ const failed = bad[bad.length - 1];
+ assert.strictEqual(failed.type, 'result');
+ assert.strictEqual(failed.is_error, true);
+ assert.match(failed.result, /the plan ran out/);
+ });
+
+ await check('Claude Code on another provider: the endpoint and the model come from the connection', async () => {
+ const key = fake.providerKey();
+ const endpoint = 'https://provider.example/anthropic';
+ const events = await run(cli.claudeOther, { prompt: 'fix the test', cwd: f.dir, permission: 'edit' },
+ { PATH: f.bin, FAKE_RECORD: f.record, ANTHROPIC_BASE_URL: endpoint, ANTHROPIC_AUTH_TOKEN: key, ANTHROPIC_MODEL: 'provider-model-x' });
+ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));
+ assert.strictEqual(rec.env.ANTHROPIC_BASE_URL, endpoint, 'the endpoint of the connection');
+ assert.strictEqual(rec.env.ANTHROPIC_AUTH_TOKEN, key, 'the key of the connection');
+ assert.strictEqual(rec.env.ANTHROPIC_MODEL, 'provider-model-x', 'the model id of the connection');
+ assert.ok(!rec.argv.join(' ').includes(key), 'the key is never in argv');
+ assert.ok(!rec.argv.join(' ').toLowerCase().includes('minimax'), 'no provider is hard-coded');
+ assert.strictEqual(argOf(rec.argv, '--output-format'), 'stream-json');
+ assert.strictEqual(argOf(rec.argv, '--permission-mode'), 'acceptEdits');
+ assert.strictEqual(rec.stdin, 'fix the test', 'the brief rides in on stdin');
+ const result = events[events.length - 1];
+ assert.strictEqual(result.type, 'result');
+ assert.strictEqual(result.is_error, false, result.result);
+ assert.strictEqual(result.result, 'claude did it');
+ });
+
+ await check('Claude Code on another provider: a resume continues the session', async () => {
+ await run(cli.claudeOther, { prompt: 'x', cwd: f.dir, permission: 'edit', resume: 'cl-9' },
+ { PATH: f.bin, FAKE_RECORD: f.record, ANTHROPIC_BASE_URL: 'https://provider.example/anthropic', ANTHROPIC_AUTH_TOKEN: fake.providerKey() });
+ const rec = JSON.parse(fs.readFileSync(f.record, 'utf8'));
+ assert.strictEqual(argOf(rec.argv, '--resume'), 'cl-9');
+ });
+
+ await check('detection: the binary that is not there, and the one that is', () => {
+ const missing = availability(cli.minimaxCode, { PATH: '/nonexistent' });
+ assert.strictEqual(missing.available, false);
+ assert.match(missing.reason, /not on PATH/);
+ const there = availability(cli.minimaxCode, { PATH: f.bin });
+ assert.strictEqual(there.available, true, there.reason);
+ assert.match(there.version, /mcode/, 'the version it reports about itself');
+ });
+
+ await check('detection: Claude Code on another provider needs its connection', () => {
+ const bare = availability(cli.claudeOther, { PATH: f.bin });
+ assert.strictEqual(bare.available, false);
+ assert.match(bare.reason, /ANTHROPIC_BASE_URL/);
+ const noToken = availability(cli.claudeOther, { PATH: f.bin, ANTHROPIC_BASE_URL: 'https://provider.example/anthropic' });
+ assert.strictEqual(noToken.available, false);
+ assert.match(noToken.reason, /ANTHROPIC_AUTH_TOKEN/);
+ const ready = availability(cli.claudeOther, { PATH: f.bin, ANTHROPIC_BASE_URL: 'https://provider.example/anthropic', ANTHROPIC_AUTH_TOKEN: fake.providerKey() });
+ assert.strictEqual(ready.available, true, ready.reason);
+ assert.match(ready.version, /Claude Code/);
+ });
+
+ await check('the harness knows both harnesses, and says why they are down', () => {
+ const harness = createHarness({ env: { PATH: f.bin } });
+ const rows = harness.agents();
+ const mm = rows.find((r) => r.id === 'minimax-code');
+ const other = rows.find((r) => r.id === 'claude-other');
+ assert.ok(mm && other, 'both are in the catalogue');
+ assert.strictEqual(mm.adapter, 'cli');
+ assert.strictEqual(mm.available, true, mm.reason);
+ assert.strictEqual(other.available, false);
+ assert.match(other.reason, /ANTHROPIC_BASE_URL/);
+ });
+
+ f.cleanup();
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });