Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 引导用户从 Kimi CLI 迁移到 Kimi Code —— 设计文档

- 日期:2026-06-05
- 状态:待评审
- 范围:在 kimi-cli(本仓库,Python 版)内新增/强化迁移引导,把用户导向继任产品 Kimi Code(独立单二进制版,仓库 https://github.com/MoonshotAI/kimi-code)

## 1. 背景与现状

- **kimi-cli**(本仓库):初代终端 Agent,PyPI 安装,数据目录 `~/.kimi/`。内部已部分改名 "Kimi Code CLI"(`constant.py: NAME`)。本仓库还带一个 `packages/kimi-code` 的 PyPI 别名包(`kimi-code` 包 → 依赖 `kimi-cli`,提供同名 `kimi-code` 命令,实际仍是旧版)。
- **kimi-code**(继任产品,独立仓库):重写的单二进制版,curl 一行安装,不依赖 Python/Node,启动飞快,主打 video input / MCP / subagents / hooks。数据目录 `~/.kimi-code/`,**启动命令同样是 `kimi`**。安装时会**自动迁移**旧版的配置与会话(已验证)。

现有引导(均偏被动):
1. `README.md` 顶部 IMPORTANT banner。
2. 文档首页 `docs/index.md` hero 已改名。
3. 运行时欢迎屏一条单行 Tip(`src/kimi_cli/app.py:771`)。

## 2. 目标与原则

- **目标**:在不制造焦虑、不强制的前提下,把"从 kimi-cli 迁到 kimi-code"的摩擦降到最低,并在用户有意图的时刻(启动、退出、主动求助)轻量、反复地提示。
- **主信息**(贯穿所有触点):*"换 Kimi Code —— 更快、单二进制;配置和会话自动迁移。"* 自动迁移已验证,是最强卖点,须放在最显眼处。
- **强度定位**:主动协助(T1 + T2),不做强制(不做倒计时 / 不做强制 interstitial / 不做到期阻断)。

## 3. 已确认的决策

| 项 | 决策 |
|---|---|
| 引导强度 | 主动协助为主(T1 强化提示 + T2 一键迁移命令) |
| 自动迁移 | 已可用且验证过 → 可放心承诺"配置/会话自动带过去" |
| 下线计划 | 不硬性下线 → 文案软化为"推荐升级、老安装继续可用",不做紧迫感倒计时 |
| `/upgrade` 行为 | 带确认地替用户执行安装脚本 |
| 退出提示 | 节流:每天最多一次 |
| 已装检测 | 检测 `~/.kimi-code/` 目录是否存在 |

安装命令(取自 kimi-code 仓库,权威来源):
- macOS/Linux:`curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash`
- Windows(PowerShell):`irm https://code.kimi.com/kimi-code/install.ps1 | iex`
- 备选(npm):`npm install -g @moonshot-ai/kimi-code`

## 4. 关键风险 / 坑(必须处理)

1. **命令名撞车**:新旧工具的启动命令都是 `kimi`,由 PATH 优先级决定谁生效。`/upgrade` 装完**不能**提示"运行 `kimi-code`",而应提示"开新终端运行 `kimi`,并用 `which kimi` 确认指向 `~/.kimi-code`"。
2. **无法热替换当前进程**:`/upgrade` 安装后,当前正在跑的仍是旧 Python 进程、PATH 也才刚改。不要尝试在会话内 exec 切换;提示用户开新终端即可。
3. **别名包误导**:本仓库的 `packages/kimi-code` 让 `pip install kimi-code` 装回的还是旧版。建议给该别名包加启动自报"这是旧版 CLI,新版在 …"(附带项,见 §5.6)。
4. **数据目录区分**:旧版 `~/.kimi/`,新版 `~/.kimi-code/`。触点②用 `~/.kimi-code/` 检测,已确认不会与旧版冲突。
5. **节流状态持久化**:退出提示的"每天一次"需要一个持久化的"上次展示日期",存放位置见 §5.4。

## 5. 触点设计

### 5.1 触点①:`/upgrade` slash 命令(核心,T2)

- **落点**:`src/kimi_cli/ui/shell/slash.py`(与现有 feedback / open-web-ui 命令同构注册)。
- **流程**:
1. 检测平台(darwin/linux → bash 安装命令;win32 → PowerShell 安装命令;其余 → 打印 npm 备选 + 文档链接)。
2. 打印**将要运行的完整安装命令**,请用户确认(默认 No)。
3. 确认后在子进程执行安装命令(继承终端,让安装器自己的交互/进度正常显示)。
4. 成功后打印后续指引(见 §6 文案);失败则打印命令让用户手动执行 + 文档链接。
- **不做**:不在会话内热替换进程;不假设安装目录可写之外的任何副作用。

### 5.2 触点②:启动检测已装 kimi-code(T2)

- **落点**:`src/kimi_cli/app.py` 构建 welcome_info 处。
- **逻辑**:若 `~/.kimi-code/` 存在 → 追加一条 welcome 项,提示"你已装好 Kimi Code,开新终端用 `kimi` 即可(`which kimi` 确认指向 `~/.kimi-code`)"。
- **语义**:本段代码只可能在旧版进程里执行;`~/.kimi-code/` 存在即"用户已装新版但当前仍在跑旧版",是强引导时机。

### 5.3 触点③:强化启动欢迎卡片(T1)

- **落点**:`src/kimi_cli/app.py:771` 现有那条单行 Tip。
- **改动**:扩成紧凑小卡 —— 标题 + 1~2 个硬卖点(单二进制 / 秒级启动 / video input)+ 一行 `/upgrade` 指引(见 §6)。保持简短,不喧宾夺主。
- 与触点②互斥/共存:若②已提示"已装",③可弱化为单行或不显示(避免重复)。实现期决定。

### 5.4 触点④:退出提示(T1,节流每天一次)

- **落点**:`src/kimi_cli/ui/shell` 的退出路径(`/quit`、`Ctrl-D`)。
- **节流**:读写一个"上次展示日期",存于 `~/.kimi/`(旧版数据目录,例如 `~/.kimi/.migration-nudge` 或 config 内字段);同一自然日只展示一次。
- **文案**:见 §6。

### 5.5 触点⑤:文档 / README 收尾(T0)

- 文档**每页**顶部 banner(改 vitepress layout / 公共组件),不只首页。
- 新增一篇《从 Kimi CLI 迁移到 Kimi Code》指南页(安装命令 + 自动迁移说明 + 命令名撞车提示 + 常见问题)。
- `README.md` 新增 "Migrating" 小节(一行命令 + 自动迁移说明)。
- **软化下线措辞**:将 README/banner 中 "will be gradually wound down" 改为"推荐升级、老安装继续可用"的口吻(对齐"不硬性下线"决策)。

### 5.6 附带项(要不要做由用户定)

- 给 `packages/kimi-code` 别名包加启动自报,堵 `pip install kimi-code` 落回旧版的坑。

## 6. UI 文案(English,产品面向英文用户)

启动欢迎卡片(触点③):
```
Kimi Code is here — the faster, single-binary successor to Kimi CLI.
• Single binary, no Python/Node • Instant startup • Video input, subagents, hooks
Run /upgrade to switch — your config & sessions migrate automatically.
```

退出提示(触点④,节流每天一次):
```
Tip: Kimi Code is faster and migrates your config & sessions automatically.
Install: curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash (or run /upgrade next time)
```

`/upgrade` 确认前(触点①):
```
This will install Kimi Code by running:
curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
Your existing config & sessions will be migrated automatically.
Proceed? [y/N]
```

`/upgrade` 成功后(触点①):
```
Kimi Code installed ✓ Your config & sessions were migrated automatically.
Open a NEW terminal and run `kimi` to start Kimi Code.
(Verify with `which kimi` — it should point inside ~/.kimi-code.)
```

已装检测(触点②):
```
Kimi Code is already installed on this machine.
Start it in a fresh terminal with `kimi` (verify: `which kimi` → ~/.kimi-code).
```

## 7. 明确不做(YAGNI)

- sunset 倒计时、首次运行强制 interstitial、到期启动阻断 —— 全部不做。
- 不在 kimi-cli 内重实现迁移逻辑(迁移由 kimi-code 安装器负责,已验证)。

## 8. 测试策略

遵循"先写失败测试再实现":
- 触点①:平台检测分支(darwin/linux/win32/其他)选对命令;确认为 No 时不执行;子进程调用被正确构造(mock 掉实际执行)。
- 触点②:`~/.kimi-code/` 存在/不存在 → welcome 项出现/不出现(用临时 HOME 隔离)。
- 触点④:节流逻辑 —— 同日第二次不展示、跨日重新展示(注入可控日期,避免依赖真实时钟)。
- 文档改动:现有文档相关测试不破。

## 9. 实现期待确认项

- 触点②/③ 共存时的去重显示策略(避免"已装提示"和"升级卡片"同屏重复)。
- npm 备选文案是否需要在 `/upgrade` 的"其他平台"分支展示。
- 退出提示节流状态的具体落盘形式(独立标记文件 vs. config 字段)。
18 changes: 8 additions & 10 deletions src/kimi_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,8 +698,6 @@ async def run_shell(
self, command: str | None = None, *, prefill_text: str | None = None
) -> bool:
"""Run the Kimi Code CLI instance with shell UI."""
from rich.text import Text

from kimi_cli.ui.shell import Shell, WelcomeInfoItem

if command is None:
Expand Down Expand Up @@ -768,16 +766,16 @@ async def run_shell(
level=WelcomeInfoItem.Level.WARN,
)
)
from kimi_cli.ui.shell.migration_nudge import (
already_installed_text,
kimi_code_installed,
welcome_card_text,
)

welcome_info.append(
WelcomeInfoItem(
name="\nTip",
value=Text.assemble(
"We just released Kimi Code — our new coding agent. Check it out at ",
Text(
"https://www.kimi.com/code",
style="link https://www.kimi.com/code underline",
),
),
name="\n✨ Update",
value=already_installed_text() if kimi_code_installed() else welcome_card_text(),
level=WelcomeInfoItem.Level.WARN,
)
)
Expand Down
11 changes: 6 additions & 5 deletions src/kimi_cli/ui/shell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from kimi_cli.ui.shell.console import console
from kimi_cli.ui.shell.echo import render_user_echo_text
from kimi_cli.ui.shell.mcp_status import render_mcp_prompt
from kimi_cli.ui.shell.migration_nudge import print_migration_goodbye
from kimi_cli.ui.shell.prompt import (
BgTaskCounts,
CustomPromptSession,
Expand Down Expand Up @@ -578,7 +579,7 @@ def _can_auto_trigger_pending() -> bool:
else:
bg_auto_failures = 0
if self._exit_after_run:
console.print("Bye!")
print_migration_goodbye(console)
break
continue

Expand All @@ -596,7 +597,7 @@ def _can_auto_trigger_pending() -> bool:
continue

if event.kind == "eof":
console.print("Bye!")
print_migration_goodbye(console)
break

if event.kind == "cwd_lost":
Expand All @@ -623,7 +624,7 @@ def _can_auto_trigger_pending() -> bool:

if self._should_exit_input(user_input):
logger.debug("Exiting by slash command")
console.print("Bye!")
print_migration_goodbye(console)
break

if user_input.mode == PromptMode.SHELL:
Expand Down Expand Up @@ -668,7 +669,7 @@ def _can_auto_trigger_pending() -> bool:
await self.run_soul_command(slash_cmd_call.raw_input)
console.print()
if self._exit_after_run:
console.print("Bye!")
print_migration_goodbye(console)
break
else:
await self._run_slash_command(slash_cmd_call)
Expand All @@ -680,7 +681,7 @@ def _can_auto_trigger_pending() -> bool:
await self.run_soul_command(user_input.content)
console.print()
if self._exit_after_run:
console.print("Bye!")
print_migration_goodbye(console)
break
finally:
prompt_task.cancel()
Expand Down
95 changes: 95 additions & 0 deletions src/kimi_cli/ui/shell/migration_nudge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import annotations

import contextlib
from datetime import date
from pathlib import Path

from rich.console import Console
from rich.text import Text

from kimi_cli.share import get_share_dir

_INSTALL_SH = "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash"
_INSTALL_PS = "irm https://code.kimi.com/kimi-code/install.ps1 | iex"


def install_command(platform: str) -> str:
"""Return the Kimi Code install command for the given sys.platform value."""
if platform == "win32":
return _INSTALL_PS
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke PowerShell explicitly for Windows upgrades

When sys.platform == "win32", /upgrade passes this bare PowerShell pipeline to _run_shell_command, which runs it through asyncio.create_subprocess_shell using the platform default shell (normally cmd.exe), where irm and iex are not commands. Windows users who accept the prompt therefore get a shell failure instead of an install; return a command that explicitly launches PowerShell or teach the caller to use PowerShell for this platform.

Useful? React with 👍 / 👎.

return _INSTALL_SH


def kimi_code_installed(home: Path | None = None) -> bool:
"""True if the standalone Kimi Code is installed (its data dir ~/.kimi-code exists)."""
home = home or Path.home()
return (home / ".kimi-code").is_dir()


def exit_nudge_marker(share_dir: Path) -> Path:
"""Path of the throttle marker recording the last day the exit nudge was shown."""
return share_dir / ".migration-nudge"


def should_show_exit_nudge(marker: Path, today: str) -> bool:
"""Return True at most once per calendar day; record `today` when returning True.

`today` is an ISO date string (e.g. "2026-06-05"), injected for testability.
"""
try:
last = marker.read_text(encoding="utf-8").strip()
except OSError:
last = ""
if last == today:
return False
with contextlib.suppress(OSError):
marker.write_text(today, encoding="utf-8")
return True


def welcome_card_text() -> Text:
"""Welcome-screen card nudging users to upgrade (shown when Kimi Code is NOT installed)."""
return Text.assemble(
"The new Kimi Code is here — rebuilt to be faster and more powerful.\n",
"Run ",
("/upgrade", "bold"),
"; your config & sessions carry over.",
)


def already_installed_text() -> Text:
"""Welcome-screen note shown when Kimi Code IS already installed on this machine."""
return Text.assemble(
"The new Kimi Code is already installed. Start it in a fresh terminal with ",
("kimi", "bold"),
" (verify: ",
("which kimi", "cyan"),
" → ~/.kimi-code).",
Comment on lines +81 to +85
)


def exit_nudge_text() -> Text:
"""Throttled tip printed on graceful exit."""
return Text.assemble(
("Tip: ", "yellow"),
"The new Kimi Code is rebuilt to be faster and more powerful.\n",
"Install: ",
(_INSTALL_SH, "cyan"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 exit_nudge_text() hardcodes the bash install command, showing wrong command on Windows

exit_nudge_text() at line 77 hardcodes _INSTALL_SH (the curl ... | bash command) in the exit nudge shown on graceful exit. On Windows, users will see a bash command they cannot use. The function should use install_command(sys.platform) to select the platform-appropriate command, as the /upgrade command correctly does at src/kimi_cli/ui/shell/slash.py:730.

Suggested change
(_INSTALL_SH, "cyan"),
(install_command(__import__("sys").platform), "cyan"),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the platform installer in the exit nudge

When the CLI exits on Windows and Kimi Code is not installed, print_migration_goodbye() prints this exit_nudge_text(), but the text hard-codes the Unix curl ... | bash installer instead of using the platform-specific command. That means Windows users get an exit prompt with a command that depends on bash and bypasses the PowerShell installer path that install_command() otherwise defines, so the migration guidance is not actionable in that environment.

Useful? React with 👍 / 👎.

(" (or run /upgrade next time)", "grey50"),
Comment on lines +92 to +96
)


def print_migration_goodbye(
console: Console, *, home: Path | None = None, today: str | None = None
) -> None:
"""Print the farewell ("Bye!") plus, at most once per day, the migration tip.

Skipped entirely (only "Bye!") if Kimi Code is already installed.
`home`/`today` are injectable for testing; in production they default to the real values.
"""
console.print("Bye!")
if kimi_code_installed(home):
return
today = today or date.today().isoformat()
if should_show_exit_nudge(exit_nudge_marker(get_share_dir()), today):
console.print(exit_nudge_text())
38 changes: 38 additions & 0 deletions src/kimi_cli/ui/shell/slash.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import sys
from collections.abc import Awaitable, Callable, Iterable
from typing import TYPE_CHECKING, Any, cast

Expand Down Expand Up @@ -718,6 +719,43 @@ def vis(app: Shell, args: str):
raise SwitchToVis(session_id=session_id)


@registry.command
async def upgrade(app: Shell, args: str):
Comment on lines +722 to +723

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register /upgrade for shell mode too

When the prompt is toggled to shell mode with Ctrl-X, _run_shell_command only dispatches slash commands found in shell_mode_registry; because /upgrade is registered only in the agent-mode registry here, a user following the new welcome/exit guidance while in shell mode gets the "not available in shell mode" warning instead of the installer. Since this command does not require agent context, it should be registered in shell mode as well.

Useful? React with 👍 / 👎.

"""Install Kimi Code — the faster successor (migrates your config & sessions)"""
from kimi_cli.telemetry import track
from kimi_cli.ui.shell.migration_nudge import install_command

track("upgrade_invoked")

cmd = install_command(sys.platform)
console.print(
"[bold]This will install the new Kimi Code by running:[/bold]\n"
f" [cyan]{cmd}[/cyan]\n"
"Your existing config & sessions will be migrated automatically."
)
try:
choice = await ChoiceInput(
message="Proceed with installation? (↑↓ navigate, Enter select, Ctrl+C cancel):",
options=[("yes", "Yes, install now"), ("no", "No, just show me the command")],
default="yes",
).prompt_async()
except (EOFError, KeyboardInterrupt):
console.print("[grey50]Upgrade cancelled.[/grey50]")
return

if choice != "yes":
console.print(f"No problem. To install later, run:\n [cyan]{cmd}[/cyan]")
return

await app._run_shell_command(cmd) # pyright: ignore[reportPrivateUsage]
console.print(
"\n[green]The new Kimi Code is installed ✓[/green] "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not report success after a failed installer

If the installer exits non-zero (for example due to no network, a 404 from the install URL, or a shell error), _run_shell_command only waits for the subprocess and returns None, so this path still prints “The new Kimi Code is installed ✓” even though nothing was installed. The upgrade flow should observe the subprocess exit status or verify ~/.kimi-code before showing the success/migration message.

Useful? React with 👍 / 👎.

"Your config & sessions were migrated automatically.\n"
Comment on lines +756 to +758

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop claiming migration has already completed

After a successful installer run, this message says config and sessions were migrated, but the Kimi Code migration flow does not run during installation: the first kimi launch prompts the user, or they can run kimi migrate manually (per the migration docs). In the normal /upgrade path where the installer succeeds and the user has not yet launched the new CLI, this falsely tells them their data is already carried over; change this to say migration will be offered on first launch or to run kimi migrate.

Useful? React with 👍 / 👎.

"Open a [bold]new terminal[/bold] and run [bold]kimi[/bold] to start it.\n"
"[grey50](Verify with `which kimi` — it should point inside ~/.kimi-code.)[/grey50]"
)
Comment on lines +757 to +762

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 /upgrade unconditionally prints success message even when installation fails

After await app._run_shell_command(cmd) at line 750, the success message ("The new Kimi Code is installed ✓") is printed unconditionally at lines 751-756. _run_shell_command (src/kimi_cli/ui/shell/__init__.py:716) returns None, swallows exceptions internally, and does not check the subprocess exit code. If the install script fails (non-zero exit), the subprocess can't be created, or the network is down, the user still sees the success message claiming installation succeeded and config was migrated.

Prompt for agents
The /upgrade command in src/kimi_cli/ui/shell/slash.py:750-756 unconditionally prints a success message after calling app._run_shell_command(cmd). However, _run_shell_command (src/kimi_cli/ui/shell/__init__.py:716) is a void method that internally catches exceptions and does not expose the subprocess exit code. There are two approaches to fix this:

1. Check for the result after running: have _run_shell_command return the process exit code (or a bool), then conditionally print the success message only on success. On failure, print the manual install command instead. This requires modifying _run_shell_command's return type or adding a variant that returns status.

2. Run the subprocess directly in the upgrade function instead of delegating to _run_shell_command, so you can inspect proc.returncode after proc.wait(). On non-zero exit or exception, show a failure message with the manual command.

The design spec (section 5.1) explicitly says: "失败则打印命令让用户手动执行 + 文档链接" (on failure, print the command for manual execution + docs link).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.



@registry.command
async def mcp(app: Shell, args: str):
"""Show MCP servers and tools"""
Expand Down
Loading
Loading