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.

Files changed (137) hide show
  1. kimi_cli/CHANGELOG.md +349 -40
  2. kimi_cli/__init__.py +6 -0
  3. kimi_cli/acp/AGENTS.md +91 -0
  4. kimi_cli/acp/__init__.py +13 -0
  5. kimi_cli/acp/convert.py +111 -0
  6. kimi_cli/acp/kaos.py +270 -0
  7. kimi_cli/acp/mcp.py +46 -0
  8. kimi_cli/acp/server.py +335 -0
  9. kimi_cli/acp/session.py +445 -0
  10. kimi_cli/acp/tools.py +158 -0
  11. kimi_cli/acp/types.py +13 -0
  12. kimi_cli/agents/default/agent.yaml +4 -4
  13. kimi_cli/agents/default/sub.yaml +2 -1
  14. kimi_cli/agents/default/system.md +79 -21
  15. kimi_cli/agents/okabe/agent.yaml +17 -0
  16. kimi_cli/agentspec.py +53 -25
  17. kimi_cli/app.py +180 -52
  18. kimi_cli/cli/__init__.py +595 -0
  19. kimi_cli/cli/__main__.py +8 -0
  20. kimi_cli/cli/info.py +63 -0
  21. kimi_cli/cli/mcp.py +349 -0
  22. kimi_cli/config.py +153 -17
  23. kimi_cli/constant.py +3 -0
  24. kimi_cli/exception.py +23 -2
  25. kimi_cli/flow/__init__.py +117 -0
  26. kimi_cli/flow/d2.py +376 -0
  27. kimi_cli/flow/mermaid.py +218 -0
  28. kimi_cli/llm.py +129 -23
  29. kimi_cli/metadata.py +32 -7
  30. kimi_cli/platforms.py +262 -0
  31. kimi_cli/prompts/__init__.py +2 -0
  32. kimi_cli/prompts/compact.md +4 -5
  33. kimi_cli/session.py +223 -31
  34. kimi_cli/share.py +2 -0
  35. kimi_cli/skill.py +145 -0
  36. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  37. kimi_cli/skills/skill-creator/SKILL.md +351 -0
  38. kimi_cli/soul/__init__.py +51 -20
  39. kimi_cli/soul/agent.py +213 -85
  40. kimi_cli/soul/approval.py +86 -17
  41. kimi_cli/soul/compaction.py +64 -53
  42. kimi_cli/soul/context.py +38 -5
  43. kimi_cli/soul/denwarenji.py +2 -0
  44. kimi_cli/soul/kimisoul.py +442 -60
  45. kimi_cli/soul/message.py +54 -54
  46. kimi_cli/soul/slash.py +72 -0
  47. kimi_cli/soul/toolset.py +387 -6
  48. kimi_cli/toad.py +74 -0
  49. kimi_cli/tools/AGENTS.md +5 -0
  50. kimi_cli/tools/__init__.py +42 -34
  51. kimi_cli/tools/display.py +25 -0
  52. kimi_cli/tools/dmail/__init__.py +10 -10
  53. kimi_cli/tools/dmail/dmail.md +11 -9
  54. kimi_cli/tools/file/__init__.py +1 -3
  55. kimi_cli/tools/file/glob.py +20 -23
  56. kimi_cli/tools/file/grep.md +1 -1
  57. kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
  58. kimi_cli/tools/file/read.md +24 -6
  59. kimi_cli/tools/file/read.py +134 -50
  60. kimi_cli/tools/file/replace.md +1 -1
  61. kimi_cli/tools/file/replace.py +36 -29
  62. kimi_cli/tools/file/utils.py +282 -0
  63. kimi_cli/tools/file/write.py +43 -22
  64. kimi_cli/tools/multiagent/__init__.py +7 -0
  65. kimi_cli/tools/multiagent/create.md +11 -0
  66. kimi_cli/tools/multiagent/create.py +50 -0
  67. kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
  68. kimi_cli/tools/shell/__init__.py +120 -0
  69. kimi_cli/tools/{bash → shell}/bash.md +1 -2
  70. kimi_cli/tools/shell/powershell.md +25 -0
  71. kimi_cli/tools/test.py +4 -4
  72. kimi_cli/tools/think/__init__.py +2 -2
  73. kimi_cli/tools/todo/__init__.py +14 -8
  74. kimi_cli/tools/utils.py +64 -24
  75. kimi_cli/tools/web/fetch.py +68 -13
  76. kimi_cli/tools/web/search.py +10 -12
  77. kimi_cli/ui/acp/__init__.py +65 -412
  78. kimi_cli/ui/print/__init__.py +37 -49
  79. kimi_cli/ui/print/visualize.py +179 -0
  80. kimi_cli/ui/shell/__init__.py +141 -84
  81. kimi_cli/ui/shell/console.py +2 -0
  82. kimi_cli/ui/shell/debug.py +28 -23
  83. kimi_cli/ui/shell/keyboard.py +5 -1
  84. kimi_cli/ui/shell/prompt.py +220 -194
  85. kimi_cli/ui/shell/replay.py +111 -46
  86. kimi_cli/ui/shell/setup.py +89 -82
  87. kimi_cli/ui/shell/slash.py +422 -0
  88. kimi_cli/ui/shell/update.py +4 -2
  89. kimi_cli/ui/shell/usage.py +271 -0
  90. kimi_cli/ui/shell/visualize.py +574 -72
  91. kimi_cli/ui/wire/__init__.py +267 -0
  92. kimi_cli/ui/wire/jsonrpc.py +142 -0
  93. kimi_cli/ui/wire/protocol.py +1 -0
  94. kimi_cli/utils/__init__.py +0 -0
  95. kimi_cli/utils/aiohttp.py +2 -0
  96. kimi_cli/utils/aioqueue.py +72 -0
  97. kimi_cli/utils/broadcast.py +37 -0
  98. kimi_cli/utils/changelog.py +12 -7
  99. kimi_cli/utils/clipboard.py +12 -0
  100. kimi_cli/utils/datetime.py +37 -0
  101. kimi_cli/utils/environment.py +58 -0
  102. kimi_cli/utils/envvar.py +12 -0
  103. kimi_cli/utils/frontmatter.py +44 -0
  104. kimi_cli/utils/logging.py +7 -6
  105. kimi_cli/utils/message.py +9 -14
  106. kimi_cli/utils/path.py +99 -9
  107. kimi_cli/utils/pyinstaller.py +6 -0
  108. kimi_cli/utils/rich/__init__.py +33 -0
  109. kimi_cli/utils/rich/columns.py +99 -0
  110. kimi_cli/utils/rich/markdown.py +961 -0
  111. kimi_cli/utils/rich/markdown_sample.md +108 -0
  112. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  113. kimi_cli/utils/signals.py +2 -0
  114. kimi_cli/utils/slashcmd.py +124 -0
  115. kimi_cli/utils/string.py +2 -0
  116. kimi_cli/utils/term.py +168 -0
  117. kimi_cli/utils/typing.py +20 -0
  118. kimi_cli/wire/__init__.py +98 -29
  119. kimi_cli/wire/serde.py +45 -0
  120. kimi_cli/wire/types.py +299 -0
  121. kimi_cli-0.78.dist-info/METADATA +200 -0
  122. kimi_cli-0.78.dist-info/RECORD +135 -0
  123. kimi_cli-0.78.dist-info/entry_points.txt +4 -0
  124. kimi_cli/cli.py +0 -250
  125. kimi_cli/soul/runtime.py +0 -96
  126. kimi_cli/tools/bash/__init__.py +0 -99
  127. kimi_cli/tools/file/patch.md +0 -8
  128. kimi_cli/tools/file/patch.py +0 -143
  129. kimi_cli/tools/mcp.py +0 -85
  130. kimi_cli/ui/shell/liveview.py +0 -386
  131. kimi_cli/ui/shell/metacmd.py +0 -262
  132. kimi_cli/wire/message.py +0 -91
  133. kimi_cli-0.44.dist-info/METADATA +0 -188
  134. kimi_cli-0.44.dist-info/RECORD +0 -89
  135. kimi_cli-0.44.dist-info/entry_points.txt +0 -3
  136. /kimi_cli/tools/{task → multiagent}/task.md +0 -0
  137. {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
@@ -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
@@ -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
- )