从 Claude Code、Codex 与 OpenCode 迁移
三种外部会话怎样读进 MiMo 并按修改时间增量重导,“从 Claude Code 导入”搬走哪些凭据,以及哪些 Claude Code 资产是直接沿用的。
从别的编码 Agent 换到 MiMo,要带走的东西分两类。一类是历史会话:Claude Code、Codex、OpenCode 各有自己的本地存储,MiMo 把它们读进自己的 SQLite,在会话列表里和原生会话并排出现。另一类是配置与凭据:API key、CLAUDE.md、自定义命令、MCP 服务器、技能。前者靠三个导入器,后者大部分不导入,而是每次启动时直接去读原来的位置。
怎么用
| 来源 | 读取位置 | 何时导入 | 入口 |
|---|---|---|---|
| Claude Code | ~/.claude/projects/*/*.jsonl | 每次启动 CLI 自动执行 | mimo session import-claude [--force] |
| Codex | ~/.codex/sessions/**/*.jsonl | 只能经 HTTP 接口触发 | POST /global/import/run |
| OpenCode | $XDG_DATA_HOME/opencode/opencode.db,未设置时为 ~/.local/share/opencode/opencode.db | 只能经 HTTP 接口触发 | 同上 |
- 自动导入可以用环境变量
MIMOCODE_DISABLE_CLAUDE_IMPORT关掉,设成任何非空值都算。MIMOCODE_MIMO_ONLY管不到它。 GET /global/import/scan只读地统计三个来源是否存在、各有多少会话、已导入多少;POST /global/import/run的请求体可带sources(cc、codex、opencode的子集)与force,某个来源失败时错误写进该来源的errors,不影响其他来源(packages/opencode/src/server/routes/global.ts:298)。仓库里除了生成的 SDK 方法,没有找到调用这两个接口的前端代码。- 凭据迁移在 TUI 里:
/login(或/connect)对话框的“从 Claude Code 导入”。
启动时的自动导入写在 CLI 的全局中间件里,紧跟数据库迁移之后(packages/opencode/src/index.ts:160):
// Idempotently import Claude Code sessions into SQLite. Runs once per process
// tree (the env guard is inherited by spawned children) and is best-effort:
// a failure here must never block command startup.
if (!process.env.MIMOCODE_DISABLE_CLAUDE_IMPORT && !process.env.MIMOCODE_CLAUDE_IMPORTED) {
process.env.MIMOCODE_CLAUDE_IMPORTED = "1"
try {
await ClaudeImport.run()
} catch (e) {
Log.Default.warn("claude-import failed", { e: errorMessage(e) })
}
}它先把 MIMOCODE_CLAUDE_IMPORTED 写进环境变量,子进程继承后就不会再导一遍。每次启动都会遍历全部 JSONL,但没变过的文件只比较一次修改时间就跳过。
记账表:external_import
三个导入器共用一张表记录“哪个外部会话对应哪个 MiMo 会话”(packages/opencode/src/session/external-import.sql.ts:6):
export const ExternalImportTable = sqliteTable(
"external_import",
{
source: text().$type<ExternalSource>().notNull(),
source_key: text().notNull(),
session_id: text().$type<SessionID>().notNull(),
source_path: text().notNull(),
source_mtime: integer().notNull(),
time_imported: integer().notNull(),
message_ids: text({ mode: "json" }).$type<MessageID[]>(),
},
(table) => [primaryKey({ columns: [table.source, table.source_key] })],
)这张表最早叫 claude_import,只服务 Claude Code;后来的迁移把它推广成三个来源,旧数据的来源一律记为 cc(packages/opencode/migration/20260612000000_external_import/migration.sql:12)。
每个导入器对单个外部会话的处理顺序相同,以 Claude Code 为例(packages/opencode/src/session/claude-import.ts:232):
- 跳过未变的:记账表里已有这条记录、修改时间也没变,且没传
force,就跳过(claude-import.ts:251)。Claude Code 与 Codex 比较文件的 mtime,OpenCode 比较会话的time_updated。 - 处理被删的会话:记录还在,但对应的 MiMo 会话已被用户删掉,就丢掉这条记录当作全新导入,否则后面的更新影响零行、插入消息时外键失败(
claude-import.ts:256)。 - 解析并确定项目:会话的工作目录存在且在 git 仓库里,就按 MiMo 的规则算出项目 ID,否则归到全局项目(
claude-import.ts:226)。 - 在一个事务里写库:项目行不存在才插入;新会话插入会话行;重导时只删除上次导入的那批消息,更新会话的目录与版本(
claude-import.ts:297);逐条写入消息与部件;最后更新记账表,连同这次的消息 ID 列表。
第 4 步里重导的那段(claude-import.ts:297):
if (existing) {
// Remove only the rows this importer previously created — never the
// user's mimocode-native continuation messages in the same session.
// Legacy rows (imported before message_ids tracking) fall back to a
// full session wipe, matching the original Claude-only contents.
if (existing.message_ids?.length) {
for (let i = 0; i < existing.message_ids.length; i += 500)
tx.delete(MessageTable)
.where(inArray(MessageTable.id, existing.message_ids.slice(i, i + 500)))
.run()
} else {
tx.delete(MessageTable).where(eq(MessageTable.session_id, sessionId)).run()
}
// Preserve mimocode-owned metadata on re-sync: keep any user rename
// (don't reset title — the Claude title is the immutable first prompt),
// and never move time_updated backward past native activity.
tx.update(SessionTable)
.set({
project_id: project.id,
directory: parsed.cwd,
version: parsed.version ?? "claude-code",
time_updated: Math.max(existingUpdated ?? 0, parsed.timeUpdated),
})
.where(eq(SessionTable.id, sessionId))
.run()记下 message_ids 的意义就在这里:导入的会话可以在 MiMo 里接着聊,之后外部文件又变了(比如又在 Claude Code 里续了几轮),重导只替换导入过来的那批消息,MiMo 里续写的消息保留,用户改过的标题也保留。部件表对消息表设了级联删除,删消息时部件随之删除(packages/opencode/src/session/session.sql.ts:108)。
ExternalImport.runAll 按 cc、codex、opencode 的顺序串行调用三个导入器,统计扫描、新导入、重导、跳过的数量与错误(packages/opencode/src/session/external-import.ts:95)。
Claude Code:逐行翻译 JSONL
Claude Code 按项目把会话存成 JSONL,每行一条记录。导入器只看 type 为 user 与 assistant 的行(claude-import.ts:84),把它们翻译成 MiMo 的 MessageV2 消息与部件:
| Claude Code 记录 | MiMo 里变成 |
|---|---|
| user 行里的文本块、图片块 | 一条 user 消息,文本部件与 file 部件(base64 图片转成 data URL) |
user 行里的 tool_result 块 | 回填到之前那个工具部件,状态改为 completed 或 error,不单独成消息 |
assistant 行里的 thinking 块 | reasoning 部件 |
assistant 行里的 text 块 | text 部件 |
assistant 行里的 tool_use 块 | tool 部件,先置为 pending,等对应的结果回填 |
几条值得知道的规则:
- 一轮合并成一条 assistant 消息。Claude Code 一轮里调多次工具会写出多条 assistant 行,中间夹着只含工具结果的 user 行。只含工具结果的 user 行不会结束当前 assistant 消息,于是同一轮的多条 assistant 行归并到同一条消息里(
claude-import.ts:169)。用量字段每遇到一条 assistant 行就整体覆盖,最终留下的是这一轮最后一条记录的 usage,而不是累加。 - 模型。
message.model不含斜杠时提供方记为anthropic;user 消息沿用上一条 assistant 的模型,会话开头的 user 消息记为anthropic/unknown(claude-import.ts:25)。费用记 0,推理 token 记 0。 - 标题。取第一段不以
<开头的用户文本,压缩空白后截取 100 个字符,这样可以跳过斜杠命令留下的标签;一段都没有时用Claude Code session(claude-import.ts:144)。 - ID。会话 ID 在首次导入时由 MiMo 生成,之后沿用;消息与部件的 ID 每次解析都重新生成,所以重导替换进来的消息是新 ID,时间戳仍沿用原始记录。
- 范围。扫描用的是
*/*.jsonl,只看项目目录下第一层的文件(claude-import.ts:237)。
Codex:response_item 流
Codex 的会话文件同样是 JSONL,但结构是一串事件。导入器读两类(packages/opencode/src/session/codex-import.ts:119):
session_meta:取工作目录,版本记为codex-加 CLI 版本号,并以其中的时间戳校正会话创建时间。response_item:按payload.type分派。message中user与developer角色都变成 user 消息,assistant的输出文本变成 text 部件;reasoning只取摘要文本;function_call与custom_tool_call变成工具部件,后者的输入原样包成raw字段;function_call_output与custom_tool_call_output回填对应工具部件的结果。
Codex 常常先写推理和工具调用、后写回答文本,甚至在任何 assistant 消息之前就出现,所以导入器用 ensureAssistant 在需要时补出 user 与 assistant 消息,注释写明了这一点(codex-import.ts:77):
Codex emits reasoning and tool calls before the assistant text — and sometimes before any assistant message at all — so we must materialize the assistant here rather than dropping the part when nothing is open yet.
与 Claude Code 相比,信息损失更多:模型一律记为 openai/unknown,用量全部为 0,工具结果一律记为 completed。developer 角色的消息也按 user 消息导入。去重键是文件名(codex-import.ts:278),session_meta 里的会话 ID 解析出来了,但没有用到。
OpenCode:直接搬数据库行
MiMo 的会话存储沿用自 OpenCode,这个导入器不做格式翻译,直接用只读方式打开 OpenCode 的 SQLite(packages/opencode/src/session/opencode-import.ts:82)。Bun 下用 bun:sqlite,Node 下用 node:sqlite,由 package.json 的条件导入挑选(packages/opencode/src/storage/read-sqlite.ts:1)。
- 先确认存在
session表,再按创建时间倒序读出全部会话(opencode-import.ts:89)。 - 每个会话的消息整批读出,部件按每批 200 个消息 ID 分批查询(
opencode-import.ts:148)。 - 会话、消息、部件的 ID、
slug、parent_id原样保留,消息与部件的data列解析成 JSON 后原样写入(opencode-import.ts:192)。 - 记账表的
source_mtime存会话的time_updated,所以 OpenCode 里续写过的会话下次会被重导(opencode-import.ts:108)。
需要注意的是,OpenCode 的父子会话关系被保留,但只搬会话、消息、部件三张表,待办等其他表不在导入范围内;所有消息的 agent_id 一律写成 main。
凭据:“从 Claude Code 导入”
README 说首次启动会自动引导配置(README.zh.md:41)。TUI 代码里能找到的是:尚未连接任何提供方时,命令面板把 /connect 标为推荐项(packages/opencode/src/cli/cmd/tui/app.tsx:836);没有选中模型又没有任何提供方时,提交输入会弹出上游原有的提供方列表(packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:433)。没有找到启动时自动弹出登录框的代码。
“从 Claude Code 导入”是 /login 与 /connect 对话框里的第三个推荐项,实现在 packages/opencode/src/cli/cmd/tui/component/dialog-mimo-login.tsx:73。它依次读取 ~/.claude/ 下的 settings.json、settings.local.json、settings_local.json 里的 env 段,后读到的文件优先,都没有时回落到当前进程的环境变量;然后取这几个值(dialog-mimo-login.tsx:98):
const key = resolve("ANTHROPIC_API_KEY")
const rawBaseUrl = resolve("ANTHROPIC_BASE_URL")
const baseUrl = rawBaseUrl
? rawBaseUrl.replace(/\/+$/, "").replace(/(?<!\/v1)$/, "/v1")
: undefined
// strip Claude Code context-window suffix e.g. claude-opus-4-6[1m]
const preferredModel = (
resolve("ANTHROPIC_DEFAULT_OPUS_MODEL") ?? resolve("ANTHROPIC_DEFAULT_SONNET_MODEL")
)?.replace(/\[.*\]$/, "")拿到之后:
- 把
ANTHROPIC_API_KEY存为anthropic提供方的 API key;找不到就提示“未找到 Claude Code API Key”并结束。凭据只认这一个变量,Claude Code 自己保存的登录凭据不在读取范围内。 - 把
baseURL写进全局配置的provider.anthropic.options:没设ANTHROPIC_BASE_URL时写官方地址https://api.anthropic.com/v1,设了就去掉末尾斜杠,末尾不是/v1时补上。 - 重建实例后挑模型:环境里指定的 Opus(其次是 Sonnet)模型不在目录里,就先写进配置;选中顺序是指定模型、
claude-opus-4-6、按名称排序后最后一个含 opus 的、最后一个含 sonnet 的、第一个模型(dialog-mimo-login.tsx:141)。
因为第 2 步总会写入 baseURL,之后发往 Anthropic 的请求都会经过 AnthropicProxyPlugin:删掉 anthropic-beta 请求头,读到 message_stop 事件后主动关闭流(packages/opencode/src/plugin/mimo.ts:205)。插件与登录的其余部分见模型、提供方与提示词路由。Claude Code 本身的配置方式见 Claude Code 手册。
沿用而非导入的部分
下面这些不经过导入器,MiMo 每次加载时直接读原位置,改动即时生效:
| 内容 | 读取位置 | 关闭开关 | 出处 |
|---|---|---|---|
| 指令文件 | 项目的 CLAUDE.md(没有 AGENTS.md 时,或 AGENTS.md 不足 500 个字符时一并加载),以及 ~/.claude/CLAUDE.md | MIMOCODE_DISABLE_CLAUDE_CODE_PROMPT | packages/opencode/src/session/instruction.ts:17 |
| 斜杠命令 | ~/.claude/commands 与从当前目录到仓库根各级的 .claude/commands,同名时 .mimocode 的命令覆盖它们 | MIMOCODE_DISABLE_CLAUDE_CODE_COMMANDS | packages/opencode/src/config/paths.ts:45 |
| MCP 服务器 | ~/.claude.json 与当前目录下 .claude.json 里的 mcpServers,与 MiMo 原生配置重名时跳过 | MIMOCODE_DISABLE_CLAUDE_CODE_MCP | packages/opencode/src/config/config.ts:771 |
| 技能 | .claude、.agents、.codex、.opencode 下的技能目录,见内置技能包与技能检索 | 每个目录各有一个开关 | packages/opencode/src/skill/index.ts:23 |
| 记忆 | ~/.claude/projects/<项目>/memory,建索引后可在 memory 工具里检索,见分层记忆 | 配置 memory.cc_index,默认关闭 | packages/opencode/src/memory/service.ts:40 |
| Workflow 脚本 | 各级 .claude/workflows/<名字>.js,排在 .mimocode/workflows 之后,见 Workflow 运行时 | 无 | packages/opencode/src/workflow/resolve.ts:24 |
上游 v1.14.19 已经会读 CLAUDE.md 和 .claude、.agents 下的技能,“AGENTS.md 太短时补读 CLAUDE.md”以及表里其余几项是 MiMo 加的。总开关是 MIMOCODE_MIMO_ONLY:它关掉 CLAUDE.md 与 .claude 技能的沿用,也不再凭环境变量里的 API key 点亮提供方,但特意保留 MCP 兼容,这样可以继续用 Claude Code 里配好的 MCP 服务器(packages/opencode/src/flag/flag.ts:206)。斜杠命令的开关独立于总开关(flag.ts:212)。Codex 的配置方式见 Codex 手册。
下一篇:终端体验:语音、多语言与视觉——TUI 里新增的语音输入、七种界面语言、星空背景与侧栏插件。