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
kimi_cli/ui/shell/liveview.py
DELETED
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from collections import deque
|
|
3
|
-
|
|
4
|
-
import streamingjson
|
|
5
|
-
from kosong.base.message import ToolCall, ToolCallPart
|
|
6
|
-
from kosong.tooling import ToolError, ToolOk, ToolResult, ToolReturnType
|
|
7
|
-
from rich import box
|
|
8
|
-
from rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
|
|
9
|
-
from rich.live import Live
|
|
10
|
-
from rich.markdown import Heading, Markdown
|
|
11
|
-
from rich.markup import escape
|
|
12
|
-
from rich.panel import Panel
|
|
13
|
-
from rich.spinner import Spinner
|
|
14
|
-
from rich.status import Status
|
|
15
|
-
from rich.text import Text
|
|
16
|
-
|
|
17
|
-
from kimi_cli.soul import StatusSnapshot
|
|
18
|
-
from kimi_cli.tools import extract_subtitle
|
|
19
|
-
from kimi_cli.ui.shell.console import console
|
|
20
|
-
from kimi_cli.ui.shell.keyboard import KeyEvent
|
|
21
|
-
from kimi_cli.wire.message import ApprovalRequest, ApprovalResponse
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class _ToolCallDisplay:
|
|
25
|
-
def __init__(self, tool_call: ToolCall):
|
|
26
|
-
self._tool_name = tool_call.function.name
|
|
27
|
-
self._lexer = streamingjson.Lexer()
|
|
28
|
-
if tool_call.function.arguments is not None:
|
|
29
|
-
self._lexer.append_string(tool_call.function.arguments)
|
|
30
|
-
|
|
31
|
-
self._title_markup = f"Using [blue]{self._tool_name}[/blue]"
|
|
32
|
-
self._subtitle = extract_subtitle(self._lexer, self._tool_name)
|
|
33
|
-
self._finished = False
|
|
34
|
-
self._spinner = Spinner("dots", text=self._spinner_markup)
|
|
35
|
-
self.renderable: RenderableType = Group(self._spinner)
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def finished(self) -> bool:
|
|
39
|
-
return self._finished
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def _spinner_markup(self) -> str:
|
|
43
|
-
return self._title_markup + self._subtitle_markup
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def _subtitle_markup(self) -> str:
|
|
47
|
-
subtitle = self._subtitle
|
|
48
|
-
return f"[grey50]: {escape(subtitle)}[/grey50]" if subtitle else ""
|
|
49
|
-
|
|
50
|
-
def append_args_part(self, args_part: str):
|
|
51
|
-
if self.finished:
|
|
52
|
-
return
|
|
53
|
-
self._lexer.append_string(args_part)
|
|
54
|
-
# TODO: don't extract detail if it's already stable
|
|
55
|
-
new_subtitle = extract_subtitle(self._lexer, self._tool_name)
|
|
56
|
-
if new_subtitle and new_subtitle != self._subtitle:
|
|
57
|
-
self._subtitle = new_subtitle
|
|
58
|
-
self._spinner.update(text=self._spinner_markup)
|
|
59
|
-
|
|
60
|
-
def finish(self, result: ToolReturnType):
|
|
61
|
-
"""
|
|
62
|
-
Finish the live display of a tool call.
|
|
63
|
-
After calling this, the `renderable` property should be re-rendered.
|
|
64
|
-
"""
|
|
65
|
-
self._finished = True
|
|
66
|
-
sign = "[red]✗[/red]" if isinstance(result, ToolError) else "[green]✓[/green]"
|
|
67
|
-
lines = [
|
|
68
|
-
Text.from_markup(f"{sign} Used [blue]{self._tool_name}[/blue]" + self._subtitle_markup)
|
|
69
|
-
]
|
|
70
|
-
if result.brief:
|
|
71
|
-
lines.append(
|
|
72
|
-
Text.from_markup(
|
|
73
|
-
f" {result.brief}", style="grey50" if isinstance(result, ToolOk) else "red"
|
|
74
|
-
)
|
|
75
|
-
)
|
|
76
|
-
self.renderable = Group(*lines)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class _ApprovalRequestDisplay:
|
|
80
|
-
def __init__(self, request: ApprovalRequest):
|
|
81
|
-
self.request = request
|
|
82
|
-
self.options = [
|
|
83
|
-
("Approve", ApprovalResponse.APPROVE),
|
|
84
|
-
("Approve for this session", ApprovalResponse.APPROVE_FOR_SESSION),
|
|
85
|
-
("Reject, tell Kimi CLI what to do instead", ApprovalResponse.REJECT),
|
|
86
|
-
]
|
|
87
|
-
self.selected_index = 0
|
|
88
|
-
|
|
89
|
-
def render(self) -> RenderableType:
|
|
90
|
-
"""Render the approval menu as a panel."""
|
|
91
|
-
lines = []
|
|
92
|
-
|
|
93
|
-
# Add request details
|
|
94
|
-
lines.append(
|
|
95
|
-
Text(f'{self.request.sender} is requesting approval to "{self.request.description}".')
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
lines.append(Text("")) # Empty line
|
|
99
|
-
|
|
100
|
-
# Add menu options
|
|
101
|
-
for i, (option_text, _) in enumerate(self.options):
|
|
102
|
-
if i == self.selected_index:
|
|
103
|
-
lines.append(Text(f"→ {option_text}", style="cyan"))
|
|
104
|
-
else:
|
|
105
|
-
lines.append(Text(f" {option_text}", style="grey50"))
|
|
106
|
-
|
|
107
|
-
content = Group(*lines)
|
|
108
|
-
return Panel.fit(
|
|
109
|
-
content,
|
|
110
|
-
title="[yellow]⚠ Approval Requested[/yellow]",
|
|
111
|
-
border_style="yellow",
|
|
112
|
-
padding=(1, 2),
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
def move_up(self):
|
|
116
|
-
"""Move selection up."""
|
|
117
|
-
self.selected_index = (self.selected_index - 1) % len(self.options)
|
|
118
|
-
|
|
119
|
-
def move_down(self):
|
|
120
|
-
"""Move selection down."""
|
|
121
|
-
self.selected_index = (self.selected_index + 1) % len(self.options)
|
|
122
|
-
|
|
123
|
-
def get_selected_response(self) -> ApprovalResponse:
|
|
124
|
-
"""Get the approval response based on selected option."""
|
|
125
|
-
return self.options[self.selected_index][1]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
class StepLiveView:
|
|
129
|
-
def __init__(self, status: StatusSnapshot, cancel_event: asyncio.Event | None = None):
|
|
130
|
-
# message content
|
|
131
|
-
self._line_buffer = Text("")
|
|
132
|
-
|
|
133
|
-
# tool call
|
|
134
|
-
self._tool_calls: dict[str, _ToolCallDisplay] = {}
|
|
135
|
-
self._last_tool_call: _ToolCallDisplay | None = None
|
|
136
|
-
|
|
137
|
-
# approval request
|
|
138
|
-
self._approval_queue = deque[ApprovalRequest]()
|
|
139
|
-
self._current_approval: _ApprovalRequestDisplay | None = None
|
|
140
|
-
self._reject_all_following = False
|
|
141
|
-
|
|
142
|
-
# status
|
|
143
|
-
self._status_text: Text | None = Text(
|
|
144
|
-
self._format_status(status), style="grey50", justify="right"
|
|
145
|
-
)
|
|
146
|
-
self._buffer_status: RenderableType | None = None
|
|
147
|
-
|
|
148
|
-
# cancel event for ESC key handling
|
|
149
|
-
self._cancel_event = cancel_event
|
|
150
|
-
|
|
151
|
-
def __enter__(self):
|
|
152
|
-
self._live = Live(
|
|
153
|
-
self._compose(),
|
|
154
|
-
console=console,
|
|
155
|
-
refresh_per_second=10,
|
|
156
|
-
transient=False, # leave the last frame on the screen
|
|
157
|
-
vertical_overflow="visible",
|
|
158
|
-
)
|
|
159
|
-
self._live.__enter__()
|
|
160
|
-
return self
|
|
161
|
-
|
|
162
|
-
def __exit__(self, exc_type, exc_value, traceback):
|
|
163
|
-
self._live.__exit__(exc_type, exc_value, traceback)
|
|
164
|
-
|
|
165
|
-
def _compose(self) -> RenderableType:
|
|
166
|
-
sections = []
|
|
167
|
-
if self._line_buffer:
|
|
168
|
-
sections.append(self._line_buffer)
|
|
169
|
-
if self._buffer_status:
|
|
170
|
-
sections.append(self._buffer_status)
|
|
171
|
-
for view in self._tool_calls.values():
|
|
172
|
-
sections.append(view.renderable)
|
|
173
|
-
if self._current_approval:
|
|
174
|
-
sections.append(self._current_approval.render())
|
|
175
|
-
if not sections:
|
|
176
|
-
# if there's nothing to display, do not show status bar
|
|
177
|
-
return Group()
|
|
178
|
-
# TODO: pin status bar at the bottom
|
|
179
|
-
if self._status_text:
|
|
180
|
-
sections.append(self._status_text)
|
|
181
|
-
return Group(*sections)
|
|
182
|
-
|
|
183
|
-
def _push_out(self, renderable: RenderableType):
|
|
184
|
-
"""
|
|
185
|
-
Push the renderable out of the live view to the console.
|
|
186
|
-
After this, the renderable will not be changed further.
|
|
187
|
-
"""
|
|
188
|
-
console.print(renderable)
|
|
189
|
-
|
|
190
|
-
def append_text(self, text: str):
|
|
191
|
-
lines = text.split("\n")
|
|
192
|
-
prev_is_empty = not self._line_buffer
|
|
193
|
-
for line in lines[:-1]:
|
|
194
|
-
self._push_out(self._line_buffer + line)
|
|
195
|
-
self._line_buffer.plain = ""
|
|
196
|
-
self._line_buffer.append(lines[-1])
|
|
197
|
-
if (prev_is_empty and self._line_buffer) or (not prev_is_empty and not self._line_buffer):
|
|
198
|
-
self._live.update(self._compose())
|
|
199
|
-
|
|
200
|
-
def append_tool_call(self, tool_call: ToolCall):
|
|
201
|
-
self._tool_calls[tool_call.id] = _ToolCallDisplay(tool_call)
|
|
202
|
-
self._last_tool_call = self._tool_calls[tool_call.id]
|
|
203
|
-
self._live.update(self._compose())
|
|
204
|
-
|
|
205
|
-
def append_tool_call_part(self, tool_call_part: ToolCallPart):
|
|
206
|
-
if not tool_call_part.arguments_part:
|
|
207
|
-
return
|
|
208
|
-
if self._last_tool_call is None:
|
|
209
|
-
return
|
|
210
|
-
self._last_tool_call.append_args_part(tool_call_part.arguments_part)
|
|
211
|
-
|
|
212
|
-
def append_tool_result(self, tool_result: ToolResult):
|
|
213
|
-
if view := self._tool_calls.get(tool_result.tool_call_id):
|
|
214
|
-
view.finish(tool_result.result)
|
|
215
|
-
self._live.update(self._compose())
|
|
216
|
-
|
|
217
|
-
def request_approval(self, approval_request: ApprovalRequest) -> None:
|
|
218
|
-
# If we're rejecting all following requests, reject immediately
|
|
219
|
-
if self._reject_all_following:
|
|
220
|
-
approval_request.resolve(ApprovalResponse.REJECT)
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
# Add to queue
|
|
224
|
-
self._approval_queue.append(approval_request)
|
|
225
|
-
|
|
226
|
-
# If no approval is currently being displayed, show the next one
|
|
227
|
-
if self._current_approval is None:
|
|
228
|
-
self._show_next_approval_request()
|
|
229
|
-
self._live.update(self._compose())
|
|
230
|
-
|
|
231
|
-
def _show_next_approval_request(self) -> None:
|
|
232
|
-
"""Show the next approval request from the queue."""
|
|
233
|
-
if not self._approval_queue:
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
while self._approval_queue:
|
|
237
|
-
request = self._approval_queue.popleft()
|
|
238
|
-
if request.resolved:
|
|
239
|
-
# skip resolved requests
|
|
240
|
-
continue
|
|
241
|
-
self._current_approval = _ApprovalRequestDisplay(request)
|
|
242
|
-
break
|
|
243
|
-
|
|
244
|
-
def update_status(self, status: StatusSnapshot):
|
|
245
|
-
if self._status_text is None:
|
|
246
|
-
return
|
|
247
|
-
self._status_text.plain = self._format_status(status)
|
|
248
|
-
|
|
249
|
-
def handle_keyboard_event(self, event: KeyEvent):
|
|
250
|
-
# Handle ESC key to cancel the run
|
|
251
|
-
if event == KeyEvent.ESCAPE and self._cancel_event is not None:
|
|
252
|
-
self._cancel_event.set()
|
|
253
|
-
return
|
|
254
|
-
|
|
255
|
-
if not self._current_approval:
|
|
256
|
-
# just ignore any keyboard event when there's no approval request
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
match event:
|
|
260
|
-
case KeyEvent.UP:
|
|
261
|
-
self._current_approval.move_up()
|
|
262
|
-
self._live.update(self._compose())
|
|
263
|
-
case KeyEvent.DOWN:
|
|
264
|
-
self._current_approval.move_down()
|
|
265
|
-
self._live.update(self._compose())
|
|
266
|
-
case KeyEvent.ENTER:
|
|
267
|
-
resp = self._current_approval.get_selected_response()
|
|
268
|
-
self._current_approval.request.resolve(resp)
|
|
269
|
-
if resp == ApprovalResponse.APPROVE_FOR_SESSION:
|
|
270
|
-
for request in self._approval_queue:
|
|
271
|
-
# approve all queued requests with the same action
|
|
272
|
-
if request.action == self._current_approval.request.action:
|
|
273
|
-
request.resolve(ApprovalResponse.APPROVE_FOR_SESSION)
|
|
274
|
-
elif resp == ApprovalResponse.REJECT:
|
|
275
|
-
# one rejection should stop the step immediately
|
|
276
|
-
while self._approval_queue:
|
|
277
|
-
self._approval_queue.popleft().resolve(ApprovalResponse.REJECT)
|
|
278
|
-
self._reject_all_following = True
|
|
279
|
-
self._current_approval = None
|
|
280
|
-
self._show_next_approval_request()
|
|
281
|
-
self._live.update(self._compose())
|
|
282
|
-
case _:
|
|
283
|
-
# just ignore any other keyboard event
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
def finish(self):
|
|
287
|
-
self._current_approval = None
|
|
288
|
-
for view in self._tool_calls.values():
|
|
289
|
-
if not view.finished:
|
|
290
|
-
# this should not happen, but just in case
|
|
291
|
-
view.finish(ToolOk(output=""))
|
|
292
|
-
self._live.update(self._compose())
|
|
293
|
-
|
|
294
|
-
def interrupt(self):
|
|
295
|
-
self._current_approval = None
|
|
296
|
-
for view in self._tool_calls.values():
|
|
297
|
-
if not view.finished:
|
|
298
|
-
view.finish(ToolError(message="", brief="Interrupted"))
|
|
299
|
-
self._live.update(self._compose())
|
|
300
|
-
|
|
301
|
-
@staticmethod
|
|
302
|
-
def _format_status(status: StatusSnapshot) -> str:
|
|
303
|
-
bounded = max(0.0, min(status.context_usage, 1.0))
|
|
304
|
-
return f"context: {bounded:.1%}"
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
class StepLiveViewWithMarkdown(StepLiveView):
|
|
308
|
-
# TODO: figure out a streaming implementation for this
|
|
309
|
-
|
|
310
|
-
def __init__(self, status: StatusSnapshot, cancel_event: asyncio.Event | None = None):
|
|
311
|
-
super().__init__(status, cancel_event)
|
|
312
|
-
self._pending_markdown_parts: list[str] = []
|
|
313
|
-
self._buffer_status_active = False
|
|
314
|
-
self._buffer_status_obj: Status | None = None
|
|
315
|
-
|
|
316
|
-
def append_text(self, text: str):
|
|
317
|
-
if not self._pending_markdown_parts:
|
|
318
|
-
self._show_thinking_status()
|
|
319
|
-
self._pending_markdown_parts.append(text)
|
|
320
|
-
|
|
321
|
-
def append_tool_call(self, tool_call: ToolCall):
|
|
322
|
-
self._flush_markdown()
|
|
323
|
-
super().append_tool_call(tool_call)
|
|
324
|
-
|
|
325
|
-
def finish(self):
|
|
326
|
-
self._flush_markdown()
|
|
327
|
-
super().finish()
|
|
328
|
-
|
|
329
|
-
def interrupt(self):
|
|
330
|
-
self._flush_markdown()
|
|
331
|
-
super().interrupt()
|
|
332
|
-
|
|
333
|
-
def __exit__(self, exc_type, exc_value, traceback):
|
|
334
|
-
self._flush_markdown()
|
|
335
|
-
return super().__exit__(exc_type, exc_value, traceback)
|
|
336
|
-
|
|
337
|
-
def _flush_markdown(self):
|
|
338
|
-
self._hide_thinking_status()
|
|
339
|
-
if not self._pending_markdown_parts:
|
|
340
|
-
return
|
|
341
|
-
markdown_text = "".join(self._pending_markdown_parts)
|
|
342
|
-
self._pending_markdown_parts.clear()
|
|
343
|
-
if markdown_text.strip():
|
|
344
|
-
self._push_out(_LeftAlignedMarkdown(markdown_text, justify="left"))
|
|
345
|
-
|
|
346
|
-
def _show_thinking_status(self):
|
|
347
|
-
if self._buffer_status_active:
|
|
348
|
-
return
|
|
349
|
-
self._buffer_status_active = True
|
|
350
|
-
self._line_buffer.plain = ""
|
|
351
|
-
self._buffer_status_obj = Status("Thinking...", console=console, spinner="dots")
|
|
352
|
-
self._buffer_status = self._buffer_status_obj.renderable
|
|
353
|
-
self._live.update(self._compose())
|
|
354
|
-
|
|
355
|
-
def _hide_thinking_status(self):
|
|
356
|
-
if not self._buffer_status_active:
|
|
357
|
-
return
|
|
358
|
-
self._buffer_status_active = False
|
|
359
|
-
if self._buffer_status_obj is not None:
|
|
360
|
-
self._buffer_status_obj.stop()
|
|
361
|
-
self._buffer_status = None
|
|
362
|
-
self._buffer_status_obj = None
|
|
363
|
-
self._live.update(self._compose())
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
class _LeftAlignedHeading(Heading):
|
|
367
|
-
"""Heading element with left-aligned content."""
|
|
368
|
-
|
|
369
|
-
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
370
|
-
text = self.text
|
|
371
|
-
text.justify = "left"
|
|
372
|
-
if self.tag == "h2":
|
|
373
|
-
text.stylize("bold")
|
|
374
|
-
if self.tag == "h1":
|
|
375
|
-
yield Panel(text, box=box.HEAVY, style="markdown.h1.border")
|
|
376
|
-
else:
|
|
377
|
-
if self.tag == "h2":
|
|
378
|
-
yield Text("")
|
|
379
|
-
yield text
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
class _LeftAlignedMarkdown(Markdown):
|
|
383
|
-
"""Markdown renderer that left-aligns headings."""
|
|
384
|
-
|
|
385
|
-
elements = dict(Markdown.elements)
|
|
386
|
-
elements["heading_open"] = _LeftAlignedHeading
|
kimi_cli/ui/shell/metacmd.py
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import tempfile
|
|
2
|
-
import webbrowser
|
|
3
|
-
from collections.abc import Awaitable, Callable, Sequence
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import TYPE_CHECKING, NamedTuple, overload
|
|
6
|
-
|
|
7
|
-
from kosong.base.message import Message
|
|
8
|
-
from rich.panel import Panel
|
|
9
|
-
|
|
10
|
-
import kimi_cli.prompts as prompts
|
|
11
|
-
from kimi_cli.soul.context import Context
|
|
12
|
-
from kimi_cli.soul.kimisoul import KimiSoul
|
|
13
|
-
from kimi_cli.soul.message import system
|
|
14
|
-
from kimi_cli.soul.runtime import load_agents_md
|
|
15
|
-
from kimi_cli.ui.shell.console import console
|
|
16
|
-
from kimi_cli.utils.changelog import CHANGELOG, format_release_notes
|
|
17
|
-
from kimi_cli.utils.logging import logger
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from kimi_cli.ui.shell import ShellApp
|
|
21
|
-
|
|
22
|
-
type MetaCmdFunc = Callable[["ShellApp", list[str]], None | Awaitable[None]]
|
|
23
|
-
"""
|
|
24
|
-
A function that runs as a meta command.
|
|
25
|
-
|
|
26
|
-
Raises:
|
|
27
|
-
LLMNotSet: When the LLM is not set.
|
|
28
|
-
ChatProviderError: When the LLM provider returns an error.
|
|
29
|
-
Reload: When the configuration should be reloaded.
|
|
30
|
-
asyncio.CancelledError: When the command is interrupted by user.
|
|
31
|
-
|
|
32
|
-
This is quite similar to the `Soul.run` method.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class MetaCommand(NamedTuple):
|
|
37
|
-
name: str
|
|
38
|
-
description: str
|
|
39
|
-
func: MetaCmdFunc
|
|
40
|
-
aliases: list[str]
|
|
41
|
-
kimi_soul_only: bool
|
|
42
|
-
# TODO: actually kimi_soul_only meta commands should be defined in KimiSoul
|
|
43
|
-
|
|
44
|
-
def slash_name(self):
|
|
45
|
-
"""/name (aliases)"""
|
|
46
|
-
if self.aliases:
|
|
47
|
-
return f"/{self.name} ({', '.join(self.aliases)})"
|
|
48
|
-
return f"/{self.name}"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# primary name -> MetaCommand
|
|
52
|
-
_meta_commands: dict[str, MetaCommand] = {}
|
|
53
|
-
# primary name or alias -> MetaCommand
|
|
54
|
-
_meta_command_aliases: dict[str, MetaCommand] = {}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def get_meta_command(name: str) -> MetaCommand | None:
|
|
58
|
-
return _meta_command_aliases.get(name)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def get_meta_commands() -> list[MetaCommand]:
|
|
62
|
-
"""Get all unique primary meta commands (without duplicating aliases)."""
|
|
63
|
-
return list(_meta_commands.values())
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@overload
|
|
67
|
-
def meta_command(func: MetaCmdFunc, /) -> MetaCmdFunc: ...
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@overload
|
|
71
|
-
def meta_command(
|
|
72
|
-
*,
|
|
73
|
-
name: str | None = None,
|
|
74
|
-
aliases: Sequence[str] | None = None,
|
|
75
|
-
kimi_soul_only: bool = False,
|
|
76
|
-
) -> Callable[[MetaCmdFunc], MetaCmdFunc]: ...
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def meta_command(
|
|
80
|
-
func: MetaCmdFunc | None = None,
|
|
81
|
-
*,
|
|
82
|
-
name: str | None = None,
|
|
83
|
-
aliases: Sequence[str] | None = None,
|
|
84
|
-
kimi_soul_only: bool = False,
|
|
85
|
-
) -> (
|
|
86
|
-
MetaCmdFunc
|
|
87
|
-
| Callable[
|
|
88
|
-
[MetaCmdFunc],
|
|
89
|
-
MetaCmdFunc,
|
|
90
|
-
]
|
|
91
|
-
):
|
|
92
|
-
"""Decorator to register a meta command with optional custom name and aliases.
|
|
93
|
-
|
|
94
|
-
Usage examples:
|
|
95
|
-
@meta_command
|
|
96
|
-
def help(app: App, args: list[str]): ...
|
|
97
|
-
|
|
98
|
-
@meta_command(name="run")
|
|
99
|
-
def start(app: App, args: list[str]): ...
|
|
100
|
-
|
|
101
|
-
@meta_command(aliases=["h", "?", "assist"])
|
|
102
|
-
def help(app: App, args: list[str]): ...
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
def _register(f: MetaCmdFunc):
|
|
106
|
-
primary = name or f.__name__
|
|
107
|
-
alias_list = list(aliases) if aliases else []
|
|
108
|
-
|
|
109
|
-
# Create the primary command with aliases
|
|
110
|
-
cmd = MetaCommand(
|
|
111
|
-
name=primary,
|
|
112
|
-
description=(f.__doc__ or "").strip(),
|
|
113
|
-
func=f,
|
|
114
|
-
aliases=alias_list,
|
|
115
|
-
kimi_soul_only=kimi_soul_only,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
# Register primary command
|
|
119
|
-
_meta_commands[primary] = cmd
|
|
120
|
-
_meta_command_aliases[primary] = cmd
|
|
121
|
-
|
|
122
|
-
# Register aliases pointing to the same command
|
|
123
|
-
for alias in alias_list:
|
|
124
|
-
_meta_command_aliases[alias] = cmd
|
|
125
|
-
|
|
126
|
-
return f
|
|
127
|
-
|
|
128
|
-
if func is not None:
|
|
129
|
-
return _register(func)
|
|
130
|
-
return _register
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
@meta_command(aliases=["quit"])
|
|
134
|
-
def exit(app: "ShellApp", args: list[str]):
|
|
135
|
-
"""Exit the application"""
|
|
136
|
-
# should be handled by `ShellApp`
|
|
137
|
-
raise NotImplementedError
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
_HELP_MESSAGE_FMT = """
|
|
141
|
-
[grey50]▌ Help! I need somebody. Help! Not just anybody.[/grey50]
|
|
142
|
-
[grey50]▌ Help! You know I need someone. Help![/grey50]
|
|
143
|
-
[grey50]▌ ― The Beatles, [italic]Help![/italic][/grey50]
|
|
144
|
-
|
|
145
|
-
Sure, Kimi CLI is ready to help!
|
|
146
|
-
Just send me messages and I will help you get things done!
|
|
147
|
-
|
|
148
|
-
Meta commands are also available:
|
|
149
|
-
|
|
150
|
-
[grey50]{meta_commands_md}[/grey50]
|
|
151
|
-
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@meta_command(aliases=["h", "?"])
|
|
155
|
-
def help(app: "ShellApp", args: list[str]):
|
|
156
|
-
"""Show help information"""
|
|
157
|
-
console.print(
|
|
158
|
-
Panel(
|
|
159
|
-
_HELP_MESSAGE_FMT.format(
|
|
160
|
-
meta_commands_md="\n".join(
|
|
161
|
-
f" • {command.slash_name()}: {command.description}"
|
|
162
|
-
for command in get_meta_commands()
|
|
163
|
-
)
|
|
164
|
-
).strip(),
|
|
165
|
-
title="Kimi CLI Help",
|
|
166
|
-
border_style="wheat4",
|
|
167
|
-
expand=False,
|
|
168
|
-
padding=(1, 2),
|
|
169
|
-
)
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
@meta_command
|
|
174
|
-
def version(app: "ShellApp", args: list[str]):
|
|
175
|
-
"""Show version information"""
|
|
176
|
-
from kimi_cli.constant import VERSION
|
|
177
|
-
|
|
178
|
-
console.print(f"kimi, version {VERSION}")
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
@meta_command(name="release-notes")
|
|
182
|
-
def release_notes(app: "ShellApp", args: list[str]):
|
|
183
|
-
"""Show release notes"""
|
|
184
|
-
text = format_release_notes(CHANGELOG)
|
|
185
|
-
with console.pager(styles=True):
|
|
186
|
-
console.print(Panel.fit(text, border_style="wheat4", title="Release Notes"))
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
@meta_command
|
|
190
|
-
def feedback(app: "ShellApp", args: list[str]):
|
|
191
|
-
"""Submit feedback to make Kimi CLI better"""
|
|
192
|
-
|
|
193
|
-
ISSUE_URL = "https://github.com/MoonshotAI/kimi-cli/issues"
|
|
194
|
-
if webbrowser.open(ISSUE_URL):
|
|
195
|
-
return
|
|
196
|
-
console.print(f"Please submit feedback at [underline]{ISSUE_URL}[/underline].")
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
@meta_command(kimi_soul_only=True)
|
|
200
|
-
async def init(app: "ShellApp", args: list[str]):
|
|
201
|
-
"""Analyze the codebase and generate an `AGENTS.md` file"""
|
|
202
|
-
assert isinstance(app.soul, KimiSoul)
|
|
203
|
-
|
|
204
|
-
soul_bak = app.soul
|
|
205
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
206
|
-
logger.info("Running `/init`")
|
|
207
|
-
console.print("Analyzing the codebase...")
|
|
208
|
-
tmp_context = Context(file_backend=Path(temp_dir) / "context.jsonl")
|
|
209
|
-
app.soul = KimiSoul(soul_bak._agent, soul_bak._runtime, context=tmp_context)
|
|
210
|
-
ok = await app._run_soul_command(prompts.INIT)
|
|
211
|
-
|
|
212
|
-
if ok:
|
|
213
|
-
console.print(
|
|
214
|
-
"Codebase analyzed successfully! "
|
|
215
|
-
"An [underline]AGENTS.md[/underline] file has been created."
|
|
216
|
-
)
|
|
217
|
-
else:
|
|
218
|
-
console.print("[red]Failed to analyze the codebase.[/red]")
|
|
219
|
-
|
|
220
|
-
app.soul = soul_bak
|
|
221
|
-
agents_md = load_agents_md(soul_bak._runtime.builtin_args.KIMI_WORK_DIR)
|
|
222
|
-
system_message = system(
|
|
223
|
-
"The user just ran `/init` meta command. "
|
|
224
|
-
"The system has analyzed the codebase and generated an `AGENTS.md` file. "
|
|
225
|
-
f"Latest AGENTS.md file content:\n{agents_md}"
|
|
226
|
-
)
|
|
227
|
-
await app.soul._context.append_message(Message(role="user", content=[system_message]))
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@meta_command(aliases=["reset"], kimi_soul_only=True)
|
|
231
|
-
async def clear(app: "ShellApp", args: list[str]):
|
|
232
|
-
"""Clear the context"""
|
|
233
|
-
assert isinstance(app.soul, KimiSoul)
|
|
234
|
-
|
|
235
|
-
if app.soul._context.n_checkpoints == 0:
|
|
236
|
-
console.print("[yellow]Context is empty.[/yellow]")
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
await app.soul._context.revert_to(0)
|
|
240
|
-
console.print("[green]✓[/green] Context has been cleared.")
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
@meta_command(kimi_soul_only=True)
|
|
244
|
-
async def compact(app: "ShellApp", args: list[str]):
|
|
245
|
-
"""Compact the context"""
|
|
246
|
-
assert isinstance(app.soul, KimiSoul)
|
|
247
|
-
|
|
248
|
-
if app.soul._context.n_checkpoints == 0:
|
|
249
|
-
console.print("[yellow]Context is empty.[/yellow]")
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
logger.info("Running `/compact`")
|
|
253
|
-
with console.status("[cyan]Compacting...[/cyan]"):
|
|
254
|
-
await app.soul.compact_context()
|
|
255
|
-
console.print("[green]✓[/green] Context has been compacted.")
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
from . import ( # noqa: E402
|
|
259
|
-
debug, # noqa: F401
|
|
260
|
-
setup, # noqa: F401
|
|
261
|
-
update, # noqa: F401
|
|
262
|
-
)
|