kimi-cli 0.35__py3-none-any.whl → 0.52__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.
Files changed (88) hide show
  1. kimi_cli/CHANGELOG.md +165 -0
  2. kimi_cli/__init__.py +0 -374
  3. kimi_cli/agents/{koder → default}/agent.yaml +1 -1
  4. kimi_cli/agents/{koder → default}/system.md +1 -1
  5. kimi_cli/agentspec.py +115 -0
  6. kimi_cli/app.py +208 -0
  7. kimi_cli/cli.py +321 -0
  8. kimi_cli/config.py +33 -16
  9. kimi_cli/constant.py +4 -0
  10. kimi_cli/exception.py +16 -0
  11. kimi_cli/llm.py +144 -3
  12. kimi_cli/metadata.py +6 -69
  13. kimi_cli/prompts/__init__.py +4 -0
  14. kimi_cli/session.py +103 -0
  15. kimi_cli/soul/__init__.py +130 -9
  16. kimi_cli/soul/agent.py +159 -0
  17. kimi_cli/soul/approval.py +5 -6
  18. kimi_cli/soul/compaction.py +106 -0
  19. kimi_cli/soul/context.py +1 -1
  20. kimi_cli/soul/kimisoul.py +180 -80
  21. kimi_cli/soul/message.py +6 -6
  22. kimi_cli/soul/runtime.py +96 -0
  23. kimi_cli/soul/toolset.py +3 -2
  24. kimi_cli/tools/__init__.py +35 -31
  25. kimi_cli/tools/bash/__init__.py +25 -9
  26. kimi_cli/tools/bash/cmd.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +5 -4
  28. kimi_cli/tools/file/__init__.py +8 -0
  29. kimi_cli/tools/file/glob.md +1 -1
  30. kimi_cli/tools/file/glob.py +4 -4
  31. kimi_cli/tools/file/grep.py +36 -19
  32. kimi_cli/tools/file/patch.py +52 -10
  33. kimi_cli/tools/file/read.py +6 -5
  34. kimi_cli/tools/file/replace.py +16 -4
  35. kimi_cli/tools/file/write.py +16 -4
  36. kimi_cli/tools/mcp.py +7 -4
  37. kimi_cli/tools/task/__init__.py +60 -41
  38. kimi_cli/tools/task/task.md +1 -1
  39. kimi_cli/tools/todo/__init__.py +4 -2
  40. kimi_cli/tools/utils.py +1 -1
  41. kimi_cli/tools/web/fetch.py +2 -1
  42. kimi_cli/tools/web/search.py +13 -12
  43. kimi_cli/ui/__init__.py +0 -68
  44. kimi_cli/ui/acp/__init__.py +67 -38
  45. kimi_cli/ui/print/__init__.py +46 -69
  46. kimi_cli/ui/shell/__init__.py +145 -154
  47. kimi_cli/ui/shell/console.py +27 -1
  48. kimi_cli/ui/shell/debug.py +187 -0
  49. kimi_cli/ui/shell/keyboard.py +183 -0
  50. kimi_cli/ui/shell/metacmd.py +34 -81
  51. kimi_cli/ui/shell/prompt.py +245 -28
  52. kimi_cli/ui/shell/replay.py +104 -0
  53. kimi_cli/ui/shell/setup.py +19 -19
  54. kimi_cli/ui/shell/update.py +11 -5
  55. kimi_cli/ui/shell/visualize.py +576 -0
  56. kimi_cli/ui/wire/README.md +109 -0
  57. kimi_cli/ui/wire/__init__.py +340 -0
  58. kimi_cli/ui/wire/jsonrpc.py +48 -0
  59. kimi_cli/utils/__init__.py +0 -0
  60. kimi_cli/utils/aiohttp.py +10 -0
  61. kimi_cli/utils/changelog.py +6 -2
  62. kimi_cli/utils/clipboard.py +10 -0
  63. kimi_cli/utils/message.py +15 -1
  64. kimi_cli/utils/rich/__init__.py +33 -0
  65. kimi_cli/utils/rich/markdown.py +959 -0
  66. kimi_cli/utils/rich/markdown_sample.md +108 -0
  67. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  68. kimi_cli/utils/signals.py +41 -0
  69. kimi_cli/utils/string.py +8 -0
  70. kimi_cli/utils/term.py +114 -0
  71. kimi_cli/wire/__init__.py +73 -0
  72. kimi_cli/wire/message.py +191 -0
  73. kimi_cli-0.52.dist-info/METADATA +186 -0
  74. kimi_cli-0.52.dist-info/RECORD +99 -0
  75. kimi_cli-0.52.dist-info/entry_points.txt +3 -0
  76. kimi_cli/agent.py +0 -261
  77. kimi_cli/agents/koder/README.md +0 -3
  78. kimi_cli/prompts/metacmds/__init__.py +0 -4
  79. kimi_cli/soul/wire.py +0 -101
  80. kimi_cli/ui/shell/liveview.py +0 -158
  81. kimi_cli/utils/provider.py +0 -64
  82. kimi_cli-0.35.dist-info/METADATA +0 -24
  83. kimi_cli-0.35.dist-info/RECORD +0 -76
  84. kimi_cli-0.35.dist-info/entry_points.txt +0 -3
  85. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  86. /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
  87. /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
  88. {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
@@ -0,0 +1,183 @@
1
+ import asyncio
2
+ import sys
3
+ import threading
4
+ import time
5
+ from collections.abc import AsyncGenerator, Callable
6
+ from enum import Enum, auto
7
+
8
+
9
+ class KeyEvent(Enum):
10
+ UP = auto()
11
+ DOWN = auto()
12
+ LEFT = auto()
13
+ RIGHT = auto()
14
+ ENTER = auto()
15
+ ESCAPE = auto()
16
+ TAB = auto()
17
+
18
+
19
+ async def listen_for_keyboard() -> AsyncGenerator[KeyEvent]:
20
+ loop = asyncio.get_running_loop()
21
+ queue = asyncio.Queue[KeyEvent]()
22
+ cancel_event = threading.Event()
23
+
24
+ def emit(event: KeyEvent) -> None:
25
+ # print(f"emit: {event}")
26
+ loop.call_soon_threadsafe(queue.put_nowait, event)
27
+
28
+ listener = threading.Thread(
29
+ target=_listen_for_keyboard_thread,
30
+ args=(cancel_event, emit),
31
+ name="kimi-cli-keyboard-listener",
32
+ daemon=True,
33
+ )
34
+ listener.start()
35
+
36
+ try:
37
+ while True:
38
+ yield await queue.get()
39
+ finally:
40
+ cancel_event.set()
41
+ if listener.is_alive():
42
+ await asyncio.to_thread(listener.join)
43
+
44
+
45
+ def _listen_for_keyboard_thread(
46
+ cancel: threading.Event,
47
+ emit: Callable[[KeyEvent], None],
48
+ ) -> None:
49
+ if sys.platform == "win32":
50
+ _listen_for_keyboard_windows(cancel, emit)
51
+ else:
52
+ _listen_for_keyboard_unix(cancel, emit)
53
+
54
+
55
+ def _listen_for_keyboard_unix(
56
+ cancel: threading.Event,
57
+ emit: Callable[[KeyEvent], None],
58
+ ) -> None:
59
+ if sys.platform == "win32":
60
+ raise RuntimeError("Unix keyboard listener requires a non-Windows platform")
61
+
62
+ import termios
63
+
64
+ # make stdin raw and non-blocking
65
+ fd = sys.stdin.fileno()
66
+ oldterm = termios.tcgetattr(fd)
67
+ newattr = termios.tcgetattr(fd)
68
+ newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
69
+ newattr[6][termios.VMIN] = 0
70
+ newattr[6][termios.VTIME] = 0
71
+ termios.tcsetattr(fd, termios.TCSANOW, newattr)
72
+
73
+ try:
74
+ while not cancel.is_set():
75
+ try:
76
+ c = sys.stdin.buffer.read(1)
77
+ except (OSError, ValueError):
78
+ c = b""
79
+
80
+ if not c:
81
+ if cancel.is_set():
82
+ break
83
+ time.sleep(0.01)
84
+ continue
85
+
86
+ if c == b"\x1b":
87
+ sequence = c
88
+ for _ in range(2):
89
+ if cancel.is_set():
90
+ break
91
+ try:
92
+ fragment = sys.stdin.buffer.read(1)
93
+ except (OSError, ValueError):
94
+ fragment = b""
95
+ if not fragment:
96
+ break
97
+ sequence += fragment
98
+ if sequence in _ARROW_KEY_MAP:
99
+ break
100
+
101
+ event = _ARROW_KEY_MAP.get(sequence)
102
+ if event is not None:
103
+ emit(event)
104
+ elif sequence == b"\x1b":
105
+ emit(KeyEvent.ESCAPE)
106
+ elif c in (b"\r", b"\n"):
107
+ emit(KeyEvent.ENTER)
108
+ elif c == b"\t":
109
+ emit(KeyEvent.TAB)
110
+ finally:
111
+ # restore the terminal settings
112
+ termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
113
+
114
+
115
+ def _listen_for_keyboard_windows(
116
+ cancel: threading.Event,
117
+ emit: Callable[[KeyEvent], None],
118
+ ) -> None:
119
+ if sys.platform != "win32":
120
+ raise RuntimeError("Windows keyboard listener requires a Windows platform")
121
+
122
+ import msvcrt
123
+
124
+ while not cancel.is_set():
125
+ if msvcrt.kbhit():
126
+ c = msvcrt.getch()
127
+
128
+ # Handle special keys (arrow keys, etc.)
129
+ if c in (b"\x00", b"\xe0"):
130
+ # Extended key, read the next byte
131
+ extended = msvcrt.getch()
132
+ event = _WINDOWS_KEY_MAP.get(extended)
133
+ if event is not None:
134
+ emit(event)
135
+ elif c == b"\x1b":
136
+ sequence = c
137
+ for _ in range(2):
138
+ if cancel.is_set():
139
+ break
140
+ fragment = msvcrt.getch() if msvcrt.kbhit() else b""
141
+ if not fragment:
142
+ break
143
+ sequence += fragment
144
+ if sequence in _ARROW_KEY_MAP:
145
+ break
146
+
147
+ event = _ARROW_KEY_MAP.get(sequence)
148
+ if event is not None:
149
+ emit(event)
150
+ elif sequence == b"\x1b":
151
+ emit(KeyEvent.ESCAPE)
152
+ elif c in (b"\r", b"\n"):
153
+ emit(KeyEvent.ENTER)
154
+ elif c == b"\t":
155
+ emit(KeyEvent.TAB)
156
+ else:
157
+ if cancel.is_set():
158
+ break
159
+ time.sleep(0.01)
160
+
161
+
162
+ _ARROW_KEY_MAP: dict[bytes, KeyEvent] = {
163
+ b"\x1b[A": KeyEvent.UP,
164
+ b"\x1b[B": KeyEvent.DOWN,
165
+ b"\x1b[C": KeyEvent.RIGHT,
166
+ b"\x1b[D": KeyEvent.LEFT,
167
+ }
168
+
169
+ _WINDOWS_KEY_MAP: dict[bytes, KeyEvent] = {
170
+ b"H": KeyEvent.UP, # Up arrow
171
+ b"P": KeyEvent.DOWN, # Down arrow
172
+ b"M": KeyEvent.RIGHT, # Right arrow
173
+ b"K": KeyEvent.LEFT, # Left arrow
174
+ }
175
+
176
+
177
+ if __name__ == "__main__":
178
+
179
+ async def dev_main():
180
+ async for event in listen_for_keyboard():
181
+ print(event)
182
+
183
+ asyncio.run(dev_main())
@@ -2,19 +2,17 @@ import tempfile
2
2
  import webbrowser
3
3
  from collections.abc import Awaitable, Callable, Sequence
4
4
  from pathlib import Path
5
- from string import Template
6
5
  from typing import TYPE_CHECKING, NamedTuple, overload
7
6
 
8
- from kosong.base import generate
9
- from kosong.base.message import ContentPart, Message, TextPart
7
+ from kosong.message import Message
10
8
  from rich.panel import Panel
11
9
 
12
- import kimi_cli.prompts.metacmds as prompts
13
- from kimi_cli.agent import load_agents_md
14
- from kimi_cli.soul import LLMNotSet
10
+ import kimi_cli.prompts as prompts
11
+ from kimi_cli.cli import Reload
15
12
  from kimi_cli.soul.context import Context
16
13
  from kimi_cli.soul.kimisoul import KimiSoul
17
14
  from kimi_cli.soul.message import system
15
+ from kimi_cli.soul.runtime import load_agents_md
18
16
  from kimi_cli.ui.shell.console import console
19
17
  from kimi_cli.utils.changelog import CHANGELOG, format_release_notes
20
18
  from kimi_cli.utils.logging import logger
@@ -23,6 +21,17 @@ if TYPE_CHECKING:
23
21
  from kimi_cli.ui.shell import ShellApp
24
22
 
25
23
  type MetaCmdFunc = Callable[["ShellApp", list[str]], None | Awaitable[None]]
24
+ """
25
+ A function that runs as a meta command.
26
+
27
+ Raises:
28
+ LLMNotSet: When the LLM is not set.
29
+ ChatProviderError: When the LLM provider returns an error.
30
+ Reload: When the configuration should be reloaded.
31
+ asyncio.CancelledError: When the command is interrupted by user.
32
+
33
+ This is quite similar to the `Soul.run` method.
34
+ """
26
35
 
27
36
 
28
37
  class MetaCommand(NamedTuple):
@@ -165,15 +174,15 @@ def help(app: "ShellApp", args: list[str]):
165
174
  @meta_command
166
175
  def version(app: "ShellApp", args: list[str]):
167
176
  """Show version information"""
168
- from kimi_cli import __version__
177
+ from kimi_cli.constant import VERSION
169
178
 
170
- console.print(f"kimi, version {__version__}")
179
+ console.print(f"kimi, version {VERSION}")
171
180
 
172
181
 
173
182
  @meta_command(name="release-notes")
174
183
  def release_notes(app: "ShellApp", args: list[str]):
175
184
  """Show release notes"""
176
- text = format_release_notes(CHANGELOG)
185
+ text = format_release_notes(CHANGELOG, include_lib_changes=False)
177
186
  with console.pager(styles=True):
178
187
  console.print(Panel.fit(text, border_style="wheat4", title="Release Notes"))
179
188
 
@@ -188,25 +197,18 @@ def feedback(app: "ShellApp", args: list[str]):
188
197
  console.print(f"Please submit feedback at [underline]{ISSUE_URL}[/underline].")
189
198
 
190
199
 
191
- @meta_command
200
+ @meta_command(kimi_soul_only=True)
192
201
  async def init(app: "ShellApp", args: list[str]):
193
202
  """Analyze the codebase and generate an `AGENTS.md` file"""
194
- soul_bak = app.soul
195
- if not isinstance(soul_bak, KimiSoul):
196
- console.print("[red]Failed to analyze the codebase.[/red]")
197
- return
203
+ assert isinstance(app.soul, KimiSoul)
198
204
 
205
+ soul_bak = app.soul
199
206
  with tempfile.TemporaryDirectory() as temp_dir:
200
207
  logger.info("Running `/init`")
201
208
  console.print("Analyzing the codebase...")
202
209
  tmp_context = Context(file_backend=Path(temp_dir) / "context.jsonl")
203
- app.soul = KimiSoul(
204
- soul_bak._agent,
205
- soul_bak._agent_globals,
206
- context=tmp_context,
207
- loop_control=soul_bak._loop_control,
208
- )
209
- ok = await app._run(prompts.INIT)
210
+ app.soul = KimiSoul(soul_bak._agent, soul_bak._runtime, context=tmp_context)
211
+ ok = await app._run_soul_command(prompts.INIT, thinking=False)
210
212
 
211
213
  if ok:
212
214
  console.print(
@@ -217,7 +219,7 @@ async def init(app: "ShellApp", args: list[str]):
217
219
  console.print("[red]Failed to analyze the codebase.[/red]")
218
220
 
219
221
  app.soul = soul_bak
220
- agents_md = load_agents_md(soul_bak._agent_globals.builtin_args.KIMI_WORK_DIR)
222
+ agents_md = load_agents_md(soul_bak._runtime.builtin_args.KIMI_WORK_DIR)
221
223
  system_message = system(
222
224
  "The user just ran `/init` meta command. "
223
225
  "The system has analyzed the codebase and generated an `AGENTS.md` file. "
@@ -232,78 +234,29 @@ async def clear(app: "ShellApp", args: list[str]):
232
234
  assert isinstance(app.soul, KimiSoul)
233
235
 
234
236
  if app.soul._context.n_checkpoints == 0:
235
- console.print("[yellow]Context is empty.[/yellow]")
236
- return
237
+ raise Reload()
237
238
 
238
239
  await app.soul._context.revert_to(0)
239
- console.print("[green]✓[/green] Context has been cleared.")
240
+ raise Reload()
240
241
 
241
242
 
242
- @meta_command
243
+ @meta_command(kimi_soul_only=True)
243
244
  async def compact(app: "ShellApp", args: list[str]):
244
245
  """Compact the context"""
245
246
  assert isinstance(app.soul, KimiSoul)
246
247
 
247
- logger.info("Running `/compact`")
248
-
249
- if app.soul._agent_globals.llm is None:
250
- raise LLMNotSet()
251
-
252
- # Get current context history
253
- current_history = list(app.soul._context.history)
254
- if len(current_history) <= 1:
255
- console.print("[yellow]Context is too short to compact.[/yellow]")
248
+ if app.soul._context.n_checkpoints == 0:
249
+ console.print("[yellow]Context is empty.[/yellow]")
256
250
  return
257
251
 
258
- # Convert history to string for the compact prompt
259
- history_text = "\n\n".join(
260
- f"## Message {i + 1}\nRole: {msg.role}\nContent: {msg.content}"
261
- for i, msg in enumerate(current_history)
262
- )
263
-
264
- # Build the compact prompt using string template
265
- compact_template = Template(prompts.COMPACT)
266
- compact_prompt = compact_template.substitute(CONTEXT=history_text)
267
-
268
- # Create input message for compaction
269
- compact_message = Message(role="user", content=compact_prompt)
270
-
271
- # Call generate to get the compacted context
272
- try:
273
- with console.status("[cyan]Compacting...[/cyan]"):
274
- compacted_msg, usage = await generate(
275
- chat_provider=app.soul._agent_globals.llm.chat_provider,
276
- system_prompt="You are a helpful assistant that compacts conversation context.",
277
- tools=[],
278
- history=[compact_message],
279
- )
280
-
281
- # Clear the context and add the compacted message as the first message
282
- await app.soul._context.revert_to(0)
283
- content: list[ContentPart] = (
284
- [TextPart(text=compacted_msg.content)]
285
- if isinstance(compacted_msg.content, str)
286
- else compacted_msg.content
287
- )
288
- content.insert(
289
- 0, system("Previous context has been compacted. Here is the compaction output:")
290
- )
291
- await app.soul._context.append_message(Message(role="assistant", content=content))
292
-
293
- console.print("[green]✓[/green] Context has been compacted.")
294
- if usage:
295
- logger.info(
296
- "Compaction used {input} input tokens and {output} output tokens",
297
- input=usage.input,
298
- output=usage.output,
299
- )
300
- except Exception as e:
301
- logger.error("Failed to compact context: {error}", error=e)
302
- console.print(f"[red]Failed to compact the context: {e}[/red]")
303
- return
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.")
304
256
 
305
257
 
306
258
  from . import ( # noqa: E402
259
+ debug, # noqa: F401
307
260
  setup, # noqa: F401
308
261
  update, # noqa: F401
309
262
  )