Skip to content
Merged
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
5 changes: 5 additions & 0 deletions apps/emdash-desktop/src/assets/images/mimocode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const AGENT_PROVIDER_IDS = [
'qoder',
'letta',
'autohand',
'mimocode',
] as const;

export type AgentProviderId = (typeof AGENT_PROVIDER_IDS)[number];
Expand Down Expand Up @@ -709,6 +710,27 @@ export const AGENT_PROVIDERS: AgentProviderDefinition[] = [
alt: 'Autohand Code CLI',
terminalOnly: true,
},
{
id: 'mimocode',
name: 'MiMo Code',
description:
"Xiaomi's terminal-native coding agent with persistent cross-session memory and OpenAI-compatible provider support.",
docUrl: 'https://github.com/XiaomiMiMo/MiMo-Code',
installCommand: 'npm install -g @mimo-ai/cli',
commands: ['mimo'],
versionArgs: ['--version'],
cli: 'mimo',
autoApproveViaEnv: true,
initialPromptFlag: '--prompt',
resumeFlag: '--session',
sessionIdFlag: '--session',
sessionIdOnResumeOnly: true,
resumeWithoutSessionFlag: '--continue',
icon: 'mimocode.svg',
alt: 'MiMo Code CLI',
terminalOnly: true,
supportsHooks: true,
},
];

const PROVIDER_MAP = new Map<string, AgentProviderDefinition>(
Expand Down
86 changes: 86 additions & 0 deletions packages/core/src/agents/plugins/helpers/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
crushMcpAdapter,
droidMcpAdapter,
grokMcpAdapter,
mimocodeMcpAdapter,
opencodeMcpAdapter,
passthroughMcpAdapter,
} from './mcp';
Expand Down Expand Up @@ -554,6 +555,91 @@ describe('opencodeMcpAdapter', () => {
});
});

// ── MiMo Code adapter ───────────────────────────────────────────────────────

describe('mimocodeMcpAdapter', () => {
const adapter = mimocodeMcpAdapter();

it('writes global MiMo Code MCP config using the OpenCode-compatible schema', async () => {
const fs = createMemoryFs();

await adapter.writeServers(fs, [
{
name: 'playwright',
command: 'npx',
args: ['-y', '@playwright/mcp'],
env: { BROWSER: 'chromium' },
enabled: false,
},
{
name: 'docs',
transport: 'http',
type: 'http',
url: 'https://example.com/mcp',
headers: { Authorization: 'Bearer token' },
timeout: 30_000,
},
]);

const raw = await fs.read('.config/mimocode/mimocode.json');
expect(raw).not.toBeNull();
const parsed = JSON.parse(raw!) as { mcp: Record<string, Record<string, unknown>> };

expect(parsed.mcp.playwright).toEqual({
type: 'local',
command: ['npx', '-y', '@playwright/mcp'],
enabled: false,
environment: { BROWSER: 'chromium' },
});
expect(parsed.mcp.docs).toEqual({
type: 'remote',
url: 'https://example.com/mcp',
enabled: true,
headers: {
Authorization: 'Bearer token',
Accept: 'application/json, text/event-stream',
},
timeout: 30_000,
});
});

it('reads lower-priority MiMo Code config paths', async () => {
const fs = createMemoryFs({
'.config/mimocode/config.json': jsonFile({
mcp: {
globalLegacy: {
type: 'local',
command: ['node', 'server.js'],
},
},
}),
'.mimocode/mimocode.json': jsonFile({
mcp: {
local: {
type: 'local',
command: ['npx', '-y', '@local/mcp'],
environment: { LOCAL: '1' },
},
},
}),
});

await expect(adapter.readServers(fs)).resolves.toEqual([
{
name: 'globalLegacy',
command: 'node',
args: ['server.js'],
},
{
name: 'local',
command: 'npx',
args: ['-y', '@local/mcp'],
env: { LOCAL: '1' },
},
]);
});
});

// ── Crush adapter ───────────────────────────────────────────────────────────

describe('crushMcpAdapter', () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/agents/plugins/helpers/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,19 @@ export function opencodeMcpAdapter(
});
}

/**
* MiMo Code adapter — OpenCode fork sharing the same MCP config schema
* (type:'remote'/url for HTTP; type:'local'/command[] for stdio).
* Write: ~/.config/mimocode/mimocode.json; read lower-priority config.json and
* project-local .mimocode/mimocode.json for existing installs.
*/
export function mimocodeMcpAdapter(
configPath = '.config/mimocode/mimocode.json',
legacyReadPaths = ['.config/mimocode/config.json', '.mimocode/mimocode.json']
) {
return opencodeMcpAdapter(configPath, legacyReadPaths);
}

/**
* Copilot adapter — injects `tools: ['*']` on write; strips it on read.
* Write: ~/.copilot/mcp-config.json; legacy read: ~/.config/github-copilot/mcp.json.
Expand Down
12 changes: 12 additions & 0 deletions packages/plugins/src/agents/impl/mimocode/icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { AgentIconAsset } from '@emdash/core/agents/plugins';

export const icon: AgentIconAsset = {
kind: 'svg',
alt: 'MiMo Code CLI',
variants: [
{
minSize: 0,
light: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MiMo Code</title><g transform="translate(12 12) scale(0.62) translate(-20 -20)"><path d="M17.3598 5.03522C13.6378 5.20577 11.3905 5.78765 9.47432 7.07179C8.10992 7.97471 7.10668 9.14849 6.38435 10.6935C5.59179 12.389 5.24066 14.255 5.09017 17.7262C4.90959 21.6087 5.17043 25.4812 5.75231 27.5378C6.27399 29.4039 7.10668 30.8285 8.38079 32.0223C9.95588 33.487 11.7216 34.2595 14.4604 34.6709C16.9685 35.0421 22.7773 35.0621 25.3154 34.711C28.4054 34.2896 30.3718 33.4168 31.987 31.7414C32.7896 30.9187 33.1608 30.377 33.6824 29.2835C34.4951 27.5679 34.8261 25.7922 34.9867 22.2407C35.107 19.7026 34.9867 16.0909 34.7358 14.4055C34.1439 10.4527 32.4585 7.88441 29.519 6.39962C27.964 5.62713 25.6766 5.17567 22.4963 5.02519C20.0986 4.91483 19.7776 4.91483 17.3598 5.03522ZM21.0617 14.3854C22.386 14.6964 23.0281 15.1378 23.4494 16.0407C23.8808 16.9637 23.951 17.7262 23.951 21.8896V25.8022L22.5264 25.7721L21.0918 25.742L21.0417 21.9297C20.9915 17.7663 20.9714 17.6359 20.3895 17.1844C19.8679 16.7731 19.4264 16.7229 16.5772 16.7129H13.8685L13.8183 21.2275L13.7682 25.742H10.9591L10.929 20.0336C10.909 15.5291 10.939 14.2951 11.0293 14.2349C11.0996 14.1847 13.2365 14.1647 15.7747 14.1847C19.4967 14.2249 20.52 14.265 21.0617 14.3854ZM28.9472 14.3252C29.0174 14.4657 29.0475 25.3708 28.9773 25.722C28.9773 25.7621 28.3252 25.7822 27.5426 25.7721L26.108 25.742L26.0779 20.0838C26.0679 15.9906 26.0879 14.3854 26.1682 14.2951C26.2585 14.1847 26.6096 14.1546 27.5727 14.1546C28.6964 14.1546 28.8669 14.1747 28.9472 14.3252ZM18.9349 22.2307V25.8022L17.4601 25.7721L15.9753 25.742L15.9452 22.331C15.9352 20.455 15.9452 18.8598 15.9753 18.7996C16.0054 18.6993 16.3967 18.6692 17.4802 18.6692H18.9349V22.2307Z" fill="currentColor"/></g></svg>`,
},
],
};
88 changes: 88 additions & 0 deletions packages/plugins/src/agents/impl/mimocode/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { CommandContext } from '@emdash/core/agents/plugins';
import { describe, expect, it } from 'vitest';
import { pluginRegistry } from '../../registry';

const mimocode = pluginRegistry.get('mimocode')!;

function build(ctx: CommandContext) {
return mimocode.behavior.prompt!.buildCommand(ctx);
}

describe('mimocode plugin', () => {
it('registers current install metadata and binary name', () => {
expect(mimocode.metadata.websiteUrl).toBe('https://github.com/XiaomiMiMo/MiMo-Code');
expect(mimocode.capabilities.hostDependency.binaryNames).toEqual(['mimo']);
expect(mimocode.capabilities.hostDependency.installCommands.macos?.[0]?.command).toBe(
'npm install -g @mimo-ai/cli'
);
expect(mimocode.capabilities.hostDependency.updates).toMatchObject({
kind: 'supported',
releaseSource: { kind: 'npm', package: '@mimo-ai/cli' },
});
});

it('delivers a fresh prompt via --prompt and auto-approves via env, not a flag', () => {
const result = build({
cli: 'mimo',
autoApprove: true,
initialPrompt: 'Fix the bug',
sessionId: 'conv-1',
isResuming: false,
model: '',
});

expect(result.command).toBe('mimo');
// The interactive `mimo` TUI only accepts --prompt / -c / -s / --fork /
// --model / --agent / --never-ask-questions. Unknown flags like `--trust`
// or `--dangerously-skip-permissions` (run-only) make it print usage.
expect(result.args).toEqual(['--prompt', 'Fix the bug']);
expect(result.args).not.toContain('--dangerously-skip-permissions');
expect(result.args).not.toContain('--trust');
expect(result.env).toEqual({ MIMOCODE_PERMISSION: '{"*":"allow"}' });
});

it('omits the permission env when auto-approve is disabled', () => {
const result = build({
cli: 'mimo',
autoApprove: false,
initialPrompt: 'Fix the bug',
sessionId: 'conv-1',
isResuming: false,
model: '',
});

expect(result.args).toEqual(['--prompt', 'Fix the bug']);
expect(result.env).toEqual({});
});

it('resumes with the stored native session id via --session', () => {
const result = build({
cli: 'mimo',
autoApprove: true,
initialPrompt: '',
sessionId: 'conv-1',
providerSessionId: 'ses_abc123',
isResuming: true,
model: '',
});

expect(result.args).toEqual(['--session', 'ses_abc123']);
expect(result.env).toEqual({ MIMOCODE_PERMISSION: '{"*":"allow"}' });
});

it('falls back to --continue when only the emdash UUID is available on resume', () => {
const result = build({
cli: 'mimo',
autoApprove: true,
initialPrompt: '',
sessionId: 'conv-1',
// Seeded emdash conversation UUID — must NOT be passed as a --session value.
providerSessionId: '6fac6620-9fa8-4604-b7e0-1fe361589104',
isResuming: true,
model: '',
});

expect(result.args).toEqual(['--continue']);
expect(result.args).not.toContain('6fac6620-9fa8-4604-b7e0-1fe361589104');
});
});
75 changes: 75 additions & 0 deletions packages/plugins/src/agents/impl/mimocode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { definePlugin, registerPluginBehavior } from '@emdash/core/agents/plugins';
import {
buildStandardCommand,
createFileDropPlugin,
mimocodeMcpAdapter,
npmDependency,
} from '@emdash/core/agents/plugins/helpers';
import { icon } from './icon';
import { MIMOCODE_PLUGIN_CONTENT } from './plugin-file';

const MIMOCODE_PLUGIN_PATH = '.mimocode/plugins/emdash-notifications.js';

// MiMoCode session ids match `^ses.*` (inherited from its OpenCode base). The
// guard prevents resuming with the emdash conversation UUID that
// `conversation.sessionId` is seeded with before the first native id is captured.
const validateSessionId = (id: string) => id.startsWith('ses');

export const plugin = definePlugin(
{
id: 'mimocode',
name: 'MiMo Code',
description:
"Xiaomi's terminal-native coding agent with persistent cross-session memory and OpenAI-compatible provider support.",
websiteUrl: 'https://github.com/XiaomiMiMo/MiMo-Code',
},
{
autoApprove: {
kind: 'supported',
},
hooks: {
kind: 'plugin',
scope: 'workspace',
supportedEvents: ['notification', 'stop', 'session'],
},
hostDependency: npmDependency({ id: 'mimo', package: '@mimo-ai/cli' }),
mcp: {
kind: 'supported',
scope: 'global',
supportedTransports: ['stdio', 'http'],
},
plugins: {
kind: 'file-drop',
scope: 'workspace',
},
prompt: {
kind: 'argv',
flag: '--prompt',
},
sessions: {
kind: 'resumable',
},
},
{ icon }
);

export const provider = registerPluginBehavior(plugin, {
prompt: {
buildCommand: (ctx) =>
buildStandardCommand(ctx, {
extraEnv: ctx.autoApprove ? { MIMOCODE_PERMISSION: '{"*":"allow"}' } : {},
initialPromptFlag: '--prompt',
resumeFlag: '--session',
sessionIdFlag: '--session',
sessionIdOnResumeOnly: true,
resumeWithoutSessionFlag: '--continue',
validateSessionId,
}),
},
sessions: { validateSessionId },
mcp: mimocodeMcpAdapter(),
plugins: createFileDropPlugin({
relativePath: MIMOCODE_PLUGIN_PATH,
content: MIMOCODE_PLUGIN_CONTENT,
}),
});
Loading
Loading