kimi-cli 0.44__py3-none-any.whl → 0.78__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kimi-cli might be problematic. Click here for more details.
- kimi_cli/CHANGELOG.md +349 -40
- kimi_cli/__init__.py +6 -0
- kimi_cli/acp/AGENTS.md +91 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +111 -0
- kimi_cli/acp/kaos.py +270 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +335 -0
- kimi_cli/acp/session.py +445 -0
- kimi_cli/acp/tools.py +158 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/agents/default/agent.yaml +4 -4
- kimi_cli/agents/default/sub.yaml +2 -1
- kimi_cli/agents/default/system.md +79 -21
- kimi_cli/agents/okabe/agent.yaml +17 -0
- kimi_cli/agentspec.py +53 -25
- kimi_cli/app.py +180 -52
- kimi_cli/cli/__init__.py +595 -0
- kimi_cli/cli/__main__.py +8 -0
- kimi_cli/cli/info.py +63 -0
- kimi_cli/cli/mcp.py +349 -0
- kimi_cli/config.py +153 -17
- kimi_cli/constant.py +3 -0
- kimi_cli/exception.py +23 -2
- kimi_cli/flow/__init__.py +117 -0
- kimi_cli/flow/d2.py +376 -0
- kimi_cli/flow/mermaid.py +218 -0
- kimi_cli/llm.py +129 -23
- kimi_cli/metadata.py +32 -7
- kimi_cli/platforms.py +262 -0
- kimi_cli/prompts/__init__.py +2 -0
- kimi_cli/prompts/compact.md +4 -5
- kimi_cli/session.py +223 -31
- kimi_cli/share.py +2 -0
- kimi_cli/skill.py +145 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +351 -0
- kimi_cli/soul/__init__.py +51 -20
- kimi_cli/soul/agent.py +213 -85
- kimi_cli/soul/approval.py +86 -17
- kimi_cli/soul/compaction.py +64 -53
- kimi_cli/soul/context.py +38 -5
- kimi_cli/soul/denwarenji.py +2 -0
- kimi_cli/soul/kimisoul.py +442 -60
- kimi_cli/soul/message.py +54 -54
- kimi_cli/soul/slash.py +72 -0
- kimi_cli/soul/toolset.py +387 -6
- kimi_cli/toad.py +74 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +42 -34
- kimi_cli/tools/display.py +25 -0
- kimi_cli/tools/dmail/__init__.py +10 -10
- kimi_cli/tools/dmail/dmail.md +11 -9
- kimi_cli/tools/file/__init__.py +1 -3
- kimi_cli/tools/file/glob.py +20 -23
- kimi_cli/tools/file/grep.md +1 -1
- kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
- kimi_cli/tools/file/read.md +24 -6
- kimi_cli/tools/file/read.py +134 -50
- kimi_cli/tools/file/replace.md +1 -1
- kimi_cli/tools/file/replace.py +36 -29
- kimi_cli/tools/file/utils.py +282 -0
- kimi_cli/tools/file/write.py +43 -22
- kimi_cli/tools/multiagent/__init__.py +7 -0
- kimi_cli/tools/multiagent/create.md +11 -0
- kimi_cli/tools/multiagent/create.py +50 -0
- kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
- kimi_cli/tools/shell/__init__.py +120 -0
- kimi_cli/tools/{bash → shell}/bash.md +1 -2
- kimi_cli/tools/shell/powershell.md +25 -0
- kimi_cli/tools/test.py +4 -4
- kimi_cli/tools/think/__init__.py +2 -2
- kimi_cli/tools/todo/__init__.py +14 -8
- kimi_cli/tools/utils.py +64 -24
- kimi_cli/tools/web/fetch.py +68 -13
- kimi_cli/tools/web/search.py +10 -12
- kimi_cli/ui/acp/__init__.py +65 -412
- kimi_cli/ui/print/__init__.py +37 -49
- kimi_cli/ui/print/visualize.py +179 -0
- kimi_cli/ui/shell/__init__.py +141 -84
- kimi_cli/ui/shell/console.py +2 -0
- kimi_cli/ui/shell/debug.py +28 -23
- kimi_cli/ui/shell/keyboard.py +5 -1
- kimi_cli/ui/shell/prompt.py +220 -194
- kimi_cli/ui/shell/replay.py +111 -46
- kimi_cli/ui/shell/setup.py +89 -82
- kimi_cli/ui/shell/slash.py +422 -0
- kimi_cli/ui/shell/update.py +4 -2
- kimi_cli/ui/shell/usage.py +271 -0
- kimi_cli/ui/shell/visualize.py +574 -72
- kimi_cli/ui/wire/__init__.py +267 -0
- kimi_cli/ui/wire/jsonrpc.py +142 -0
- kimi_cli/ui/wire/protocol.py +1 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +2 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +12 -7
- kimi_cli/utils/clipboard.py +12 -0
- kimi_cli/utils/datetime.py +37 -0
- kimi_cli/utils/environment.py +58 -0
- kimi_cli/utils/envvar.py +12 -0
- kimi_cli/utils/frontmatter.py +44 -0
- kimi_cli/utils/logging.py +7 -6
- kimi_cli/utils/message.py +9 -14
- kimi_cli/utils/path.py +99 -9
- kimi_cli/utils/pyinstaller.py +6 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/markdown.py +961 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +2 -0
- kimi_cli/utils/slashcmd.py +124 -0
- kimi_cli/utils/string.py +2 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/wire/__init__.py +98 -29
- kimi_cli/wire/serde.py +45 -0
- kimi_cli/wire/types.py +299 -0
- kimi_cli-0.78.dist-info/METADATA +200 -0
- kimi_cli-0.78.dist-info/RECORD +135 -0
- kimi_cli-0.78.dist-info/entry_points.txt +4 -0
- kimi_cli/cli.py +0 -250
- kimi_cli/soul/runtime.py +0 -96
- kimi_cli/tools/bash/__init__.py +0 -99
- kimi_cli/tools/file/patch.md +0 -8
- kimi_cli/tools/file/patch.py +0 -143
- kimi_cli/tools/mcp.py +0 -85
- kimi_cli/ui/shell/liveview.py +0 -386
- kimi_cli/ui/shell/metacmd.py +0 -262
- kimi_cli/wire/message.py +0 -91
- kimi_cli-0.44.dist-info/METADATA +0 -188
- kimi_cli-0.44.dist-info/RECORD +0 -89
- kimi_cli-0.44.dist-info/entry_points.txt +0 -3
- /kimi_cli/tools/{task → multiagent}/task.md +0 -0
- {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
from kosong.message import Message
|
|
6
|
+
|
|
7
|
+
from kimi_cli.cli import OutputFormat
|
|
8
|
+
from kimi_cli.soul.message import tool_result_to_message
|
|
9
|
+
from kimi_cli.utils.aioqueue import QueueShutDown
|
|
10
|
+
from kimi_cli.wire import Wire
|
|
11
|
+
from kimi_cli.wire.types import (
|
|
12
|
+
ContentPart,
|
|
13
|
+
StepBegin,
|
|
14
|
+
StepInterrupted,
|
|
15
|
+
ToolCall,
|
|
16
|
+
ToolCallPart,
|
|
17
|
+
ToolResult,
|
|
18
|
+
WireMessage,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Printer(Protocol):
|
|
23
|
+
def feed(self, msg: WireMessage) -> None: ...
|
|
24
|
+
def flush(self) -> None: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _merge_content(buffer: list[ContentPart], part: ContentPart) -> None:
|
|
28
|
+
if not buffer or not buffer[-1].merge_in_place(part):
|
|
29
|
+
buffer.append(part)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TextPrinter(Printer):
|
|
33
|
+
def feed(self, msg: WireMessage) -> None:
|
|
34
|
+
rich.print(msg)
|
|
35
|
+
|
|
36
|
+
def flush(self) -> None:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class JsonPrinter(Printer):
|
|
41
|
+
@dataclass(slots=True)
|
|
42
|
+
class _ToolCallState:
|
|
43
|
+
tool_call: ToolCall
|
|
44
|
+
tool_result: ToolResult | None
|
|
45
|
+
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
self._content_buffer: list[ContentPart] = []
|
|
48
|
+
"""The buffer to merge content parts."""
|
|
49
|
+
self._tool_call_buffer: dict[str, JsonPrinter._ToolCallState] = {}
|
|
50
|
+
"""The buffer to store tool calls and their results."""
|
|
51
|
+
self._last_tool_call: ToolCall | None = None
|
|
52
|
+
|
|
53
|
+
def feed(self, msg: WireMessage) -> None:
|
|
54
|
+
match msg:
|
|
55
|
+
case StepBegin() | StepInterrupted():
|
|
56
|
+
self.flush()
|
|
57
|
+
case ContentPart() as part:
|
|
58
|
+
# merge with previous parts as much as possible
|
|
59
|
+
_merge_content(self._content_buffer, part)
|
|
60
|
+
case ToolCall() as call:
|
|
61
|
+
self._tool_call_buffer[call.id] = JsonPrinter._ToolCallState(
|
|
62
|
+
tool_call=call, tool_result=None
|
|
63
|
+
)
|
|
64
|
+
self._last_tool_call = call
|
|
65
|
+
case ToolCallPart() as part:
|
|
66
|
+
if self._last_tool_call is None:
|
|
67
|
+
return
|
|
68
|
+
assert self._last_tool_call.merge_in_place(part)
|
|
69
|
+
case ToolResult() as result:
|
|
70
|
+
state = self._tool_call_buffer.get(result.tool_call_id)
|
|
71
|
+
if state is None:
|
|
72
|
+
return
|
|
73
|
+
state.tool_result = result
|
|
74
|
+
case _:
|
|
75
|
+
# ignore other messages
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def flush(self) -> None:
|
|
79
|
+
if not self._content_buffer and not self._tool_call_buffer:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
tool_calls: list[ToolCall] = []
|
|
83
|
+
tool_results: list[ToolResult] = []
|
|
84
|
+
for state in self._tool_call_buffer.values():
|
|
85
|
+
if state.tool_result is None:
|
|
86
|
+
# this should only happen when interrupted
|
|
87
|
+
continue
|
|
88
|
+
tool_calls.append(state.tool_call)
|
|
89
|
+
tool_results.append(state.tool_result)
|
|
90
|
+
|
|
91
|
+
message = Message(
|
|
92
|
+
role="assistant",
|
|
93
|
+
content=self._content_buffer,
|
|
94
|
+
tool_calls=tool_calls or None,
|
|
95
|
+
)
|
|
96
|
+
print(message.model_dump_json(exclude_none=True), flush=True)
|
|
97
|
+
|
|
98
|
+
for result in tool_results:
|
|
99
|
+
# FIXME: this assumes the way how the soul convert `ToolResult` to `Message`
|
|
100
|
+
message = tool_result_to_message(result)
|
|
101
|
+
print(message.model_dump_json(exclude_none=True), flush=True)
|
|
102
|
+
|
|
103
|
+
self._content_buffer.clear()
|
|
104
|
+
self._tool_call_buffer.clear()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class FinalOnlyTextPrinter(Printer):
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
self._content_buffer: list[ContentPart] = []
|
|
110
|
+
|
|
111
|
+
def feed(self, msg: WireMessage) -> None:
|
|
112
|
+
match msg:
|
|
113
|
+
case StepBegin() | StepInterrupted():
|
|
114
|
+
self._content_buffer.clear()
|
|
115
|
+
case ContentPart() as part:
|
|
116
|
+
_merge_content(self._content_buffer, part)
|
|
117
|
+
case _:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def flush(self) -> None:
|
|
121
|
+
if not self._content_buffer:
|
|
122
|
+
return
|
|
123
|
+
message = Message(role="assistant", content=self._content_buffer)
|
|
124
|
+
text = message.extract_text()
|
|
125
|
+
if text:
|
|
126
|
+
print(text, flush=True)
|
|
127
|
+
self._content_buffer.clear()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class FinalOnlyJsonPrinter(Printer):
|
|
131
|
+
def __init__(self) -> None:
|
|
132
|
+
self._content_buffer: list[ContentPart] = []
|
|
133
|
+
|
|
134
|
+
def feed(self, msg: WireMessage) -> None:
|
|
135
|
+
match msg:
|
|
136
|
+
case StepBegin() | StepInterrupted():
|
|
137
|
+
self._content_buffer.clear()
|
|
138
|
+
case ContentPart() as part:
|
|
139
|
+
_merge_content(self._content_buffer, part)
|
|
140
|
+
case _:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def flush(self) -> None:
|
|
144
|
+
if not self._content_buffer:
|
|
145
|
+
return
|
|
146
|
+
message = Message(role="assistant", content=self._content_buffer)
|
|
147
|
+
text = message.extract_text()
|
|
148
|
+
if text:
|
|
149
|
+
final_message = Message(role="assistant", content=text)
|
|
150
|
+
print(final_message.model_dump_json(exclude_none=True), flush=True)
|
|
151
|
+
self._content_buffer.clear()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def visualize(output_format: OutputFormat, final_only: bool, wire: Wire) -> None:
|
|
155
|
+
if final_only:
|
|
156
|
+
match output_format:
|
|
157
|
+
case "text":
|
|
158
|
+
handler = FinalOnlyTextPrinter()
|
|
159
|
+
case "stream-json":
|
|
160
|
+
handler = FinalOnlyJsonPrinter()
|
|
161
|
+
else:
|
|
162
|
+
match output_format:
|
|
163
|
+
case "text":
|
|
164
|
+
handler = TextPrinter()
|
|
165
|
+
case "stream-json":
|
|
166
|
+
handler = JsonPrinter()
|
|
167
|
+
|
|
168
|
+
wire_ui = wire.ui_side(merge=True)
|
|
169
|
+
while True:
|
|
170
|
+
try:
|
|
171
|
+
msg = await wire_ui.receive()
|
|
172
|
+
except QueueShutDown:
|
|
173
|
+
handler.flush()
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
handler.feed(msg)
|
|
177
|
+
|
|
178
|
+
if isinstance(msg, StepInterrupted):
|
|
179
|
+
break
|
kimi_cli/ui/shell/__init__.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
4
|
+
import shlex
|
|
2
5
|
from collections.abc import Awaitable, Coroutine
|
|
3
6
|
from dataclasses import dataclass
|
|
4
7
|
from enum import Enum
|
|
5
8
|
from typing import Any
|
|
6
9
|
|
|
7
|
-
from kosong.base.message import ContentPart
|
|
8
10
|
from kosong.chat_provider import APIStatusError, ChatProviderError
|
|
11
|
+
from loguru import logger
|
|
9
12
|
from rich.console import Group, RenderableType
|
|
10
13
|
from rich.panel import Panel
|
|
11
14
|
from rich.table import Table
|
|
@@ -14,20 +17,34 @@ from rich.text import Text
|
|
|
14
17
|
from kimi_cli.soul import LLMNotSet, LLMNotSupported, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
15
18
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
16
19
|
from kimi_cli.ui.shell.console import console
|
|
17
|
-
from kimi_cli.ui.shell.
|
|
18
|
-
from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, ensure_new_line, toast
|
|
20
|
+
from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, toast
|
|
19
21
|
from kimi_cli.ui.shell.replay import replay_recent_history
|
|
22
|
+
from kimi_cli.ui.shell.slash import registry as shell_slash_registry
|
|
23
|
+
from kimi_cli.ui.shell.slash import shell_mode_registry
|
|
20
24
|
from kimi_cli.ui.shell.update import LATEST_VERSION_FILE, UpdateResult, do_update, semver_tuple
|
|
21
25
|
from kimi_cli.ui.shell.visualize import visualize
|
|
22
|
-
from kimi_cli.utils.
|
|
26
|
+
from kimi_cli.utils.envvar import get_env_bool
|
|
23
27
|
from kimi_cli.utils.signals import install_sigint_handler
|
|
28
|
+
from kimi_cli.utils.slashcmd import SlashCommand, SlashCommandCall, parse_slash_command_call
|
|
29
|
+
from kimi_cli.utils.term import ensure_new_line, ensure_tty_sane
|
|
30
|
+
from kimi_cli.wire.types import ContentPart, StatusUpdate
|
|
24
31
|
|
|
25
32
|
|
|
26
|
-
class
|
|
27
|
-
def __init__(self, soul: Soul, welcome_info: list[
|
|
33
|
+
class Shell:
|
|
34
|
+
def __init__(self, soul: Soul, welcome_info: list[WelcomeInfoItem] | None = None):
|
|
28
35
|
self.soul = soul
|
|
29
36
|
self._welcome_info = list(welcome_info or [])
|
|
30
37
|
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
38
|
+
self._available_slash_commands: dict[str, SlashCommand[Any]] = {
|
|
39
|
+
**{cmd.name: cmd for cmd in soul.available_slash_commands},
|
|
40
|
+
**{cmd.name: cmd for cmd in shell_slash_registry.list_commands()},
|
|
41
|
+
}
|
|
42
|
+
"""Shell-level slash commands + soul-level slash commands. Name to command mapping."""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def available_slash_commands(self) -> dict[str, SlashCommand[Any]]:
|
|
46
|
+
"""Get all available slash commands, including shell-level and soul-level commands."""
|
|
47
|
+
return self._available_slash_commands
|
|
31
48
|
|
|
32
49
|
async def run(self, command: str | None = None) -> bool:
|
|
33
50
|
if command is not None:
|
|
@@ -35,48 +52,64 @@ class ShellApp:
|
|
|
35
52
|
logger.info("Running agent with command: {command}", command=command)
|
|
36
53
|
return await self._run_soul_command(command)
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
# Start auto-update background task if not disabled
|
|
56
|
+
if get_env_bool("KIMI_CLI_NO_AUTO_UPDATE"):
|
|
57
|
+
logger.info("Auto-update disabled by KIMI_CLI_NO_AUTO_UPDATE environment variable")
|
|
58
|
+
else:
|
|
59
|
+
self._start_background_task(self._auto_update())
|
|
39
60
|
|
|
40
61
|
_print_welcome_info(self.soul.name or "Kimi CLI", self._welcome_info)
|
|
41
62
|
|
|
42
63
|
if isinstance(self.soul, KimiSoul):
|
|
43
|
-
await replay_recent_history(
|
|
64
|
+
await replay_recent_history(
|
|
65
|
+
self.soul.context.history,
|
|
66
|
+
wire_file=self.soul.wire_file,
|
|
67
|
+
)
|
|
44
68
|
|
|
45
|
-
with CustomPromptSession(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
with CustomPromptSession(
|
|
70
|
+
status_provider=lambda: self.soul.status,
|
|
71
|
+
model_capabilities=self.soul.model_capabilities or set(),
|
|
72
|
+
model_name=self.soul.model_name,
|
|
73
|
+
thinking=self.soul.thinking or False,
|
|
74
|
+
agent_mode_slash_commands=list(self._available_slash_commands.values()),
|
|
75
|
+
shell_mode_slash_commands=shell_mode_registry.list_commands(),
|
|
76
|
+
) as prompt_session:
|
|
77
|
+
try:
|
|
78
|
+
while True:
|
|
79
|
+
ensure_tty_sane()
|
|
80
|
+
try:
|
|
81
|
+
ensure_new_line()
|
|
82
|
+
user_input = await prompt_session.prompt()
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
logger.debug("Exiting by KeyboardInterrupt")
|
|
85
|
+
console.print("[grey50]Tip: press Ctrl-D or send 'exit' to quit[/grey50]")
|
|
86
|
+
continue
|
|
87
|
+
except EOFError:
|
|
88
|
+
logger.debug("Exiting by EOF")
|
|
89
|
+
console.print("Bye!")
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
if not user_input:
|
|
93
|
+
logger.debug("Got empty input, skipping")
|
|
94
|
+
continue
|
|
95
|
+
logger.debug("Got user input: {user_input}", user_input=user_input)
|
|
96
|
+
|
|
97
|
+
if user_input.command in ["exit", "quit", "/exit", "/quit"]:
|
|
98
|
+
logger.debug("Exiting by slash command")
|
|
99
|
+
console.print("Bye!")
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
if user_input.mode == PromptMode.SHELL:
|
|
103
|
+
await self._run_shell_command(user_input.command)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
if slash_cmd_call := parse_slash_command_call(user_input.command):
|
|
107
|
+
await self._run_slash_command(slash_cmd_call)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
await self._run_soul_command(user_input.content)
|
|
111
|
+
finally:
|
|
112
|
+
ensure_tty_sane()
|
|
80
113
|
|
|
81
114
|
return True
|
|
82
115
|
|
|
@@ -85,6 +118,28 @@ class ShellApp:
|
|
|
85
118
|
if not command.strip():
|
|
86
119
|
return
|
|
87
120
|
|
|
121
|
+
# Check if it's an allowed slash command in shell mode
|
|
122
|
+
if slash_cmd_call := parse_slash_command_call(command):
|
|
123
|
+
if shell_mode_registry.find_command(slash_cmd_call.name):
|
|
124
|
+
await self._run_slash_command(slash_cmd_call)
|
|
125
|
+
return
|
|
126
|
+
else:
|
|
127
|
+
console.print(
|
|
128
|
+
f'[yellow]"/{slash_cmd_call.name}" is not available in shell mode. '
|
|
129
|
+
"Press Ctrl-X to switch to agent mode.[/yellow]"
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Check if user is trying to use 'cd' command
|
|
134
|
+
stripped_cmd = command.strip()
|
|
135
|
+
split_cmd = shlex.split(stripped_cmd)
|
|
136
|
+
if len(split_cmd) == 2 and split_cmd[0] == "cd":
|
|
137
|
+
console.print(
|
|
138
|
+
"[yellow]Warning: Directory changes are not preserved across command executions."
|
|
139
|
+
"[/yellow]"
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
88
143
|
logger.info("Running shell command: {cmd}", cmd=command)
|
|
89
144
|
|
|
90
145
|
proc: asyncio.subprocess.Process | None = None
|
|
@@ -107,41 +162,37 @@ class ShellApp:
|
|
|
107
162
|
finally:
|
|
108
163
|
remove_sigint()
|
|
109
164
|
|
|
110
|
-
async def
|
|
165
|
+
async def _run_slash_command(self, command_call: SlashCommandCall) -> None:
|
|
111
166
|
from kimi_cli.cli import Reload
|
|
112
167
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
168
|
+
if command_call.name not in self._available_slash_commands:
|
|
169
|
+
logger.info("Unknown slash command /{command}", command=command_call.name)
|
|
170
|
+
console.print(
|
|
171
|
+
f'[red]Unknown slash command "/{command_call.name}", '
|
|
172
|
+
'type "/" for all available commands[/red]'
|
|
173
|
+
)
|
|
119
174
|
return
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
|
|
176
|
+
command = shell_slash_registry.find_command(command_call.name)
|
|
177
|
+
if command is None:
|
|
178
|
+
# the input is a soul-level slash command call
|
|
179
|
+
await self._run_soul_command(command_call.raw_input)
|
|
122
180
|
return
|
|
181
|
+
|
|
123
182
|
logger.debug(
|
|
124
|
-
"Running
|
|
125
|
-
|
|
126
|
-
|
|
183
|
+
"Running shell-level slash command: /{command} with args: {args}",
|
|
184
|
+
command=command_call.name,
|
|
185
|
+
args=command_call.args,
|
|
127
186
|
)
|
|
187
|
+
|
|
128
188
|
try:
|
|
129
|
-
ret = command.func(self,
|
|
189
|
+
ret = command.func(self, command_call.args)
|
|
130
190
|
if isinstance(ret, Awaitable):
|
|
131
191
|
await ret
|
|
132
|
-
except LLMNotSet:
|
|
133
|
-
logger.error("LLM not set")
|
|
134
|
-
console.print("[red]LLM not set, send /setup to configure[/red]")
|
|
135
|
-
except ChatProviderError as e:
|
|
136
|
-
logger.exception("LLM provider error:")
|
|
137
|
-
console.print(f"[red]LLM provider error: {e}[/red]")
|
|
138
|
-
except asyncio.CancelledError:
|
|
139
|
-
logger.info("Interrupted by user")
|
|
140
|
-
console.print("[red]Interrupted by user[/red]")
|
|
141
192
|
except Reload:
|
|
142
193
|
# just propagate
|
|
143
194
|
raise
|
|
144
|
-
except
|
|
195
|
+
except Exception as e:
|
|
145
196
|
logger.exception("Unknown error:")
|
|
146
197
|
console.print(f"[red]Unknown error: {e}[/red]")
|
|
147
198
|
raise # re-raise unknown error
|
|
@@ -153,6 +204,8 @@ class ShellApp:
|
|
|
153
204
|
Returns:
|
|
154
205
|
bool: Whether the run is successful.
|
|
155
206
|
"""
|
|
207
|
+
logger.info("Running soul with user input: {user_input}", user_input=user_input)
|
|
208
|
+
|
|
156
209
|
cancel_event = asyncio.Event()
|
|
157
210
|
|
|
158
211
|
def _handler():
|
|
@@ -163,25 +216,24 @@ class ShellApp:
|
|
|
163
216
|
remove_sigint = install_sigint_handler(loop, _handler)
|
|
164
217
|
|
|
165
218
|
try:
|
|
166
|
-
# Use lambda to pass cancel_event via closure
|
|
167
219
|
await run_soul(
|
|
168
220
|
self.soul,
|
|
169
221
|
user_input,
|
|
170
222
|
lambda wire: visualize(
|
|
171
|
-
wire
|
|
223
|
+
wire.ui_side(merge=False), # shell UI maintain its own merge buffer
|
|
224
|
+
initial_status=StatusUpdate(context_usage=self.soul.status.context_usage),
|
|
225
|
+
cancel_event=cancel_event,
|
|
172
226
|
),
|
|
173
227
|
cancel_event,
|
|
228
|
+
self.soul.wire_file if isinstance(self.soul, KimiSoul) else None,
|
|
174
229
|
)
|
|
175
230
|
return True
|
|
176
231
|
except LLMNotSet:
|
|
177
|
-
logger.
|
|
178
|
-
console.print(
|
|
232
|
+
logger.exception("LLM not set:")
|
|
233
|
+
console.print('[red]LLM not set, send "/setup" to configure[/red]')
|
|
179
234
|
except LLMNotSupported as e:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
model_name=e.llm.model_name,
|
|
183
|
-
capabilities=", ".join(e.capabilities),
|
|
184
|
-
)
|
|
235
|
+
# actually unsupported input/mode should already be blocked by prompt session
|
|
236
|
+
logger.exception("LLM not supported:")
|
|
185
237
|
console.print(f"[red]{e}[/red]")
|
|
186
238
|
except ChatProviderError as e:
|
|
187
239
|
logger.exception("LLM provider error:")
|
|
@@ -195,27 +247,31 @@ class ShellApp:
|
|
|
195
247
|
console.print(f"[red]LLM provider error: {e}[/red]")
|
|
196
248
|
except MaxStepsReached as e:
|
|
197
249
|
logger.warning("Max steps reached: {n_steps}", n_steps=e.n_steps)
|
|
198
|
-
console.print(f"[yellow]
|
|
250
|
+
console.print(f"[yellow]{e}[/yellow]")
|
|
199
251
|
except RunCancelled:
|
|
200
252
|
logger.info("Cancelled by user")
|
|
201
253
|
console.print("[red]Interrupted by user[/red]")
|
|
202
|
-
except
|
|
203
|
-
logger.exception("
|
|
204
|
-
console.print(f"[red]
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.exception("Unexpected error:")
|
|
256
|
+
console.print(f"[red]Unexpected error: {e}[/red]")
|
|
205
257
|
raise # re-raise unknown error
|
|
206
258
|
finally:
|
|
207
259
|
remove_sigint()
|
|
208
260
|
return False
|
|
209
261
|
|
|
210
262
|
async def _auto_update(self) -> None:
|
|
211
|
-
toast("checking for updates...", duration=2.0)
|
|
263
|
+
toast("checking for updates...", topic="update", duration=2.0)
|
|
212
264
|
result = await do_update(print=False, check_only=True)
|
|
213
265
|
if result == UpdateResult.UPDATE_AVAILABLE:
|
|
214
266
|
while True:
|
|
215
|
-
toast(
|
|
267
|
+
toast(
|
|
268
|
+
"new version found, run `uv tool upgrade kimi-cli` to upgrade",
|
|
269
|
+
topic="update",
|
|
270
|
+
duration=30.0,
|
|
271
|
+
)
|
|
216
272
|
await asyncio.sleep(60.0)
|
|
217
273
|
elif result == UpdateResult.UPDATED:
|
|
218
|
-
toast("auto updated, restart to use the new version", duration=5.0)
|
|
274
|
+
toast("auto updated, restart to use the new version", topic="update", duration=5.0)
|
|
219
275
|
|
|
220
276
|
def _start_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
|
|
221
277
|
task = asyncio.create_task(coro)
|
|
@@ -256,7 +312,7 @@ class WelcomeInfoItem:
|
|
|
256
312
|
|
|
257
313
|
|
|
258
314
|
def _print_welcome_info(name: str, info_items: list[WelcomeInfoItem]) -> None:
|
|
259
|
-
head = Text.from_markup(
|
|
315
|
+
head = Text.from_markup("Welcome to 2026, happy new year!")
|
|
260
316
|
help_text = Text.from_markup("[grey50]Send /help for help information.[/grey50]")
|
|
261
317
|
|
|
262
318
|
# Use Table for precise width control
|
|
@@ -268,7 +324,8 @@ def _print_welcome_info(name: str, info_items: list[WelcomeInfoItem]) -> None:
|
|
|
268
324
|
|
|
269
325
|
rows: list[RenderableType] = [table]
|
|
270
326
|
|
|
271
|
-
|
|
327
|
+
if info_items:
|
|
328
|
+
rows.append(Text("")) # empty line
|
|
272
329
|
for item in info_items:
|
|
273
330
|
rows.append(Text(f"{item.name}: {item.value}", style=item.level.value))
|
|
274
331
|
|
kimi_cli/ui/shell/console.py
CHANGED
kimi_cli/ui/shell/debug.py
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
from typing import TYPE_CHECKING
|
|
3
5
|
|
|
4
|
-
from kosong.
|
|
5
|
-
|
|
6
|
-
ContentPart,
|
|
7
|
-
ImageURLPart,
|
|
8
|
-
Message,
|
|
9
|
-
TextPart,
|
|
10
|
-
ThinkPart,
|
|
11
|
-
ToolCall,
|
|
12
|
-
)
|
|
13
|
-
from rich.console import Group
|
|
6
|
+
from kosong.message import Message
|
|
7
|
+
from rich.console import Group, RenderableType
|
|
14
8
|
from rich.panel import Panel
|
|
15
9
|
from rich.rule import Rule
|
|
16
10
|
from rich.syntax import Syntax
|
|
@@ -18,10 +12,19 @@ from rich.text import Text
|
|
|
18
12
|
|
|
19
13
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
20
14
|
from kimi_cli.ui.shell.console import console
|
|
21
|
-
from kimi_cli.ui.shell.
|
|
15
|
+
from kimi_cli.ui.shell.slash import registry
|
|
16
|
+
from kimi_cli.wire.types import (
|
|
17
|
+
AudioURLPart,
|
|
18
|
+
ContentPart,
|
|
19
|
+
ImageURLPart,
|
|
20
|
+
TextPart,
|
|
21
|
+
ThinkPart,
|
|
22
|
+
ToolCall,
|
|
23
|
+
VideoURLPart,
|
|
24
|
+
)
|
|
22
25
|
|
|
23
26
|
if TYPE_CHECKING:
|
|
24
|
-
from kimi_cli.ui.shell import
|
|
27
|
+
from kimi_cli.ui.shell import Shell
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
def _format_content_part(part: ContentPart) -> Text | Panel | Group:
|
|
@@ -56,6 +59,11 @@ def _format_content_part(part: ContentPart) -> Text | Panel | Group:
|
|
|
56
59
|
id_text = f" (id: {audio.id})" if audio.id else ""
|
|
57
60
|
return Text(f"[Audio{id_text}] {url_display}", style="blue")
|
|
58
61
|
|
|
62
|
+
case VideoURLPart(video_url=video):
|
|
63
|
+
url_display = video.url[:80] + "..." if len(video.url) > 80 else video.url
|
|
64
|
+
id_text = f" (id: {video.id})" if video.id else ""
|
|
65
|
+
return Text(f"[Video{id_text}] {url_display}", style="blue")
|
|
66
|
+
|
|
59
67
|
case _:
|
|
60
68
|
return Text(f"[Unknown content type: {type(part).__name__}]", style="red")
|
|
61
69
|
|
|
@@ -106,14 +114,11 @@ def _format_message(msg: Message, index: int) -> Panel:
|
|
|
106
114
|
role_text += f" [dim]→ {msg.tool_call_id}[/dim]"
|
|
107
115
|
|
|
108
116
|
# Format content
|
|
109
|
-
content_items: list = []
|
|
117
|
+
content_items: list[RenderableType] = []
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for part in msg.content:
|
|
115
|
-
formatted = _format_content_part(part)
|
|
116
|
-
content_items.append(formatted)
|
|
119
|
+
for part in msg.content:
|
|
120
|
+
formatted = _format_content_part(part)
|
|
121
|
+
content_items.append(formatted)
|
|
117
122
|
|
|
118
123
|
# Add tool calls if present
|
|
119
124
|
if msg.tool_calls:
|
|
@@ -141,12 +146,12 @@ def _format_message(msg: Message, index: int) -> Panel:
|
|
|
141
146
|
)
|
|
142
147
|
|
|
143
148
|
|
|
144
|
-
@
|
|
145
|
-
def debug(app:
|
|
149
|
+
@registry.command
|
|
150
|
+
def debug(app: Shell, args: str):
|
|
146
151
|
"""Debug the context"""
|
|
147
152
|
assert isinstance(app.soul, KimiSoul)
|
|
148
153
|
|
|
149
|
-
context = app.soul.
|
|
154
|
+
context = app.soul.context
|
|
150
155
|
history = context.history
|
|
151
156
|
|
|
152
157
|
if not history:
|
|
@@ -166,7 +171,7 @@ def debug(app: "ShellApp", args: list[str]):
|
|
|
166
171
|
Text(f"Total messages: {len(history)}", style="bold"),
|
|
167
172
|
Text(f"Token count: {context.token_count:,}", style="bold"),
|
|
168
173
|
Text(f"Checkpoints: {context.n_checkpoints}", style="bold"),
|
|
169
|
-
Text(f"Trajectory: {context.
|
|
174
|
+
Text(f"Trajectory: {context.file_backend}", style="dim"),
|
|
170
175
|
),
|
|
171
176
|
title="[bold]Context Info[/bold]",
|
|
172
177
|
border_style="cyan",
|
kimi_cli/ui/shell/keyboard.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import sys
|
|
3
5
|
import threading
|
|
@@ -5,6 +7,8 @@ import time
|
|
|
5
7
|
from collections.abc import AsyncGenerator, Callable
|
|
6
8
|
from enum import Enum, auto
|
|
7
9
|
|
|
10
|
+
from kimi_cli.utils.aioqueue import Queue
|
|
11
|
+
|
|
8
12
|
|
|
9
13
|
class KeyEvent(Enum):
|
|
10
14
|
UP = auto()
|
|
@@ -18,7 +22,7 @@ class KeyEvent(Enum):
|
|
|
18
22
|
|
|
19
23
|
async def listen_for_keyboard() -> AsyncGenerator[KeyEvent]:
|
|
20
24
|
loop = asyncio.get_running_loop()
|
|
21
|
-
queue =
|
|
25
|
+
queue = Queue[KeyEvent]()
|
|
22
26
|
cancel_event = threading.Event()
|
|
23
27
|
|
|
24
28
|
def emit(event: KeyEvent) -> None:
|