kimi-cli 0.41__py3-none-any.whl → 0.43__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 (42) hide show
  1. kimi_cli/CHANGELOG.md +28 -0
  2. kimi_cli/__init__.py +169 -102
  3. kimi_cli/agents/{koder → default}/agent.yaml +1 -1
  4. kimi_cli/agentspec.py +19 -8
  5. kimi_cli/cli.py +51 -37
  6. kimi_cli/config.py +33 -14
  7. kimi_cli/exception.py +16 -0
  8. kimi_cli/llm.py +31 -3
  9. kimi_cli/metadata.py +5 -68
  10. kimi_cli/session.py +81 -0
  11. kimi_cli/soul/__init__.py +22 -4
  12. kimi_cli/soul/agent.py +18 -23
  13. kimi_cli/soul/context.py +0 -5
  14. kimi_cli/soul/kimisoul.py +40 -25
  15. kimi_cli/soul/message.py +1 -1
  16. kimi_cli/soul/{globals.py → runtime.py} +13 -11
  17. kimi_cli/tools/file/glob.py +1 -1
  18. kimi_cli/tools/file/patch.py +1 -1
  19. kimi_cli/tools/file/read.py +1 -1
  20. kimi_cli/tools/file/replace.py +1 -1
  21. kimi_cli/tools/file/write.py +1 -1
  22. kimi_cli/tools/task/__init__.py +29 -21
  23. kimi_cli/tools/web/search.py +3 -0
  24. kimi_cli/ui/acp/__init__.py +24 -28
  25. kimi_cli/ui/print/__init__.py +27 -30
  26. kimi_cli/ui/shell/__init__.py +58 -42
  27. kimi_cli/ui/shell/keyboard.py +82 -14
  28. kimi_cli/ui/shell/metacmd.py +3 -8
  29. kimi_cli/ui/shell/prompt.py +208 -6
  30. kimi_cli/ui/shell/replay.py +104 -0
  31. kimi_cli/ui/shell/visualize.py +54 -57
  32. kimi_cli/utils/message.py +14 -0
  33. kimi_cli/utils/signals.py +41 -0
  34. kimi_cli/utils/string.py +8 -0
  35. kimi_cli/wire/__init__.py +13 -0
  36. {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/METADATA +21 -20
  37. {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/RECORD +41 -38
  38. kimi_cli/agents/koder/README.md +0 -3
  39. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  40. /kimi_cli/agents/{koder → default}/system.md +0 -0
  41. {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/WHEEL +0 -0
  42. {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/entry_points.txt +0 -0
@@ -1,28 +1,32 @@
1
1
  import asyncio
2
- import signal
3
2
  from collections.abc import Awaitable, Coroutine
3
+ from dataclasses import dataclass
4
+ from enum import Enum
4
5
  from typing import Any
5
6
 
7
+ from kosong.base.message import ContentPart
6
8
  from kosong.chat_provider import APIStatusError, ChatProviderError
7
9
  from rich.console import Group, RenderableType
8
10
  from rich.panel import Panel
9
11
  from rich.table import Table
10
12
  from rich.text import Text
11
13
 
12
- from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
14
+ from kimi_cli.soul import LLMNotSet, LLMNotSupported, MaxStepsReached, RunCancelled, Soul, run_soul
13
15
  from kimi_cli.soul.kimisoul import KimiSoul
14
16
  from kimi_cli.ui.shell.console import console
15
17
  from kimi_cli.ui.shell.metacmd import get_meta_command
16
- from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, toast
18
+ from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, ensure_new_line, toast
19
+ from kimi_cli.ui.shell.replay import replay_recent_history
17
20
  from kimi_cli.ui.shell.update import LATEST_VERSION_FILE, UpdateResult, do_update, semver_tuple
18
21
  from kimi_cli.ui.shell.visualize import visualize
19
22
  from kimi_cli.utils.logging import logger
23
+ from kimi_cli.utils.signals import install_sigint_handler
20
24
 
21
25
 
22
26
  class ShellApp:
23
- def __init__(self, soul: Soul, welcome_info: dict[str, str] | None = None):
27
+ def __init__(self, soul: Soul, welcome_info: list["WelcomeInfoItem"] | None = None):
24
28
  self.soul = soul
25
- self.welcome_info = welcome_info or {}
29
+ self._welcome_info = list(welcome_info or [])
26
30
  self._background_tasks: set[asyncio.Task[Any]] = set()
27
31
 
28
32
  async def run(self, command: str | None = None) -> bool:
@@ -31,13 +35,17 @@ class ShellApp:
31
35
  logger.info("Running agent with command: {command}", command=command)
32
36
  return await self._run_soul_command(command)
33
37
 
34
- self._start_auto_update_task()
38
+ self._start_background_task(self._auto_update())
35
39
 
36
- _print_welcome_info(self.soul.name or "Kimi CLI", self.soul.model, self.welcome_info)
40
+ _print_welcome_info(self.soul.name or "Kimi CLI", self._welcome_info)
41
+
42
+ if isinstance(self.soul, KimiSoul):
43
+ await replay_recent_history(self.soul.context.history)
37
44
 
38
45
  with CustomPromptSession(lambda: self.soul.status) as prompt_session:
39
46
  while True:
40
47
  try:
48
+ ensure_new_line()
41
49
  user_input = await prompt_session.prompt()
42
50
  except KeyboardInterrupt:
43
51
  logger.debug("Exiting by KeyboardInterrupt")
@@ -62,14 +70,13 @@ class ShellApp:
62
70
  await self._run_shell_command(user_input.command)
63
71
  continue
64
72
 
65
- command = user_input.command
66
- if command.startswith("/"):
67
- logger.debug("Running meta command: {command}", command=command)
68
- await self._run_meta_command(command[1:])
73
+ if user_input.command.startswith("/"):
74
+ logger.debug("Running meta command: {command}", command=user_input.command)
75
+ await self._run_meta_command(user_input.command[1:])
69
76
  continue
70
77
 
71
- logger.info("Running agent command: {command}", command=command)
72
- await self._run_soul_command(command)
78
+ logger.info("Running agent command: {command}", command=user_input.content)
79
+ await self._run_soul_command(user_input.content)
73
80
 
74
81
  return True
75
82
 
@@ -79,24 +86,26 @@ class ShellApp:
79
86
  return
80
87
 
81
88
  logger.info("Running shell command: {cmd}", cmd=command)
89
+
90
+ proc: asyncio.subprocess.Process | None = None
91
+
92
+ def _handler():
93
+ logger.debug("SIGINT received.")
94
+ if proc:
95
+ proc.terminate()
96
+
82
97
  loop = asyncio.get_running_loop()
98
+ remove_sigint = install_sigint_handler(loop, _handler)
83
99
  try:
84
100
  # TODO: For the sake of simplicity, we now use `create_subprocess_shell`.
85
101
  # Later we should consider making this behave like a real shell.
86
102
  proc = await asyncio.create_subprocess_shell(command)
87
-
88
- def _handler():
89
- logger.debug("SIGINT received.")
90
- proc.terminate()
91
-
92
- loop.add_signal_handler(signal.SIGINT, _handler)
93
-
94
103
  await proc.wait()
95
104
  except Exception as e:
96
105
  logger.exception("Failed to run shell command:")
97
106
  console.print(f"[red]Failed to run shell command: {e}[/red]")
98
107
  finally:
99
- loop.remove_signal_handler(signal.SIGINT)
108
+ remove_sigint()
100
109
 
101
110
  async def _run_meta_command(self, command_str: str):
102
111
  from kimi_cli.cli import Reload
@@ -137,7 +146,7 @@ class ShellApp:
137
146
  console.print(f"[red]Unknown error: {e}[/red]")
138
147
  raise # re-raise unknown error
139
148
 
140
- async def _run_soul_command(self, command: str) -> bool:
149
+ async def _run_soul_command(self, user_input: str | list[ContentPart]) -> bool:
141
150
  """
142
151
  Run the soul and handle any known exceptions.
143
152
 
@@ -151,13 +160,13 @@ class ShellApp:
151
160
  cancel_event.set()
152
161
 
153
162
  loop = asyncio.get_running_loop()
154
- loop.add_signal_handler(signal.SIGINT, _handler)
163
+ remove_sigint = install_sigint_handler(loop, _handler)
155
164
 
156
165
  try:
157
166
  # Use lambda to pass cancel_event via closure
158
167
  await run_soul(
159
168
  self.soul,
160
- command,
169
+ user_input,
161
170
  lambda wire: visualize(
162
171
  wire, initial_status=self.soul.status, cancel_event=cancel_event
163
172
  ),
@@ -167,6 +176,13 @@ class ShellApp:
167
176
  except LLMNotSet:
168
177
  logger.error("LLM not set")
169
178
  console.print("[red]LLM not set, send /setup to configure[/red]")
179
+ except LLMNotSupported as e:
180
+ logger.error(
181
+ "LLM model '{model_name}' does not support required capabilities: {capabilities}",
182
+ model_name=e.llm.model_name,
183
+ capabilities=", ".join(e.capabilities),
184
+ )
185
+ console.print(f"[red]{e}[/red]")
170
186
  except ChatProviderError as e:
171
187
  logger.exception("LLM provider error:")
172
188
  if isinstance(e, APIStatusError) and e.status_code == 401:
@@ -188,13 +204,10 @@ class ShellApp:
188
204
  console.print(f"[red]Unknown error: {e}[/red]")
189
205
  raise # re-raise unknown error
190
206
  finally:
191
- loop.remove_signal_handler(signal.SIGINT)
207
+ remove_sigint()
192
208
  return False
193
209
 
194
- def _start_auto_update_task(self) -> None:
195
- self._add_background_task(self._auto_update_background())
196
-
197
- async def _auto_update_background(self) -> None:
210
+ async def _auto_update(self) -> None:
198
211
  toast("checking for updates...", duration=2.0)
199
212
  result = await do_update(print=False, check_only=True)
200
213
  if result == UpdateResult.UPDATE_AVAILABLE:
@@ -204,7 +217,7 @@ class ShellApp:
204
217
  elif result == UpdateResult.UPDATED:
205
218
  toast("auto updated, restart to use the new version", duration=5.0)
206
219
 
207
- def _add_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
220
+ def _start_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
208
221
  task = asyncio.create_task(coro)
209
222
  self._background_tasks.add(task)
210
223
 
@@ -230,7 +243,19 @@ _LOGO = f"""\
230
243
  """
231
244
 
232
245
 
233
- def _print_welcome_info(name: str, model: str, info_items: dict[str, str]) -> None:
246
+ @dataclass(slots=True)
247
+ class WelcomeInfoItem:
248
+ class Level(Enum):
249
+ INFO = "grey50"
250
+ WARN = "yellow"
251
+ ERROR = "red"
252
+
253
+ name: str
254
+ value: str
255
+ level: Level = Level.INFO
256
+
257
+
258
+ def _print_welcome_info(name: str, info_items: list[WelcomeInfoItem]) -> None:
234
259
  head = Text.from_markup(f"[bold]Welcome to {name}![/bold]")
235
260
  help_text = Text.from_markup("[grey50]Send /help for help information.[/grey50]")
236
261
 
@@ -244,17 +269,8 @@ def _print_welcome_info(name: str, model: str, info_items: dict[str, str]) -> No
244
269
  rows: list[RenderableType] = [table]
245
270
 
246
271
  rows.append(Text("")) # Empty line
247
- rows.extend(
248
- Text.from_markup(f"[grey50]{key}: {value}[/grey50]") for key, value in info_items.items()
249
- )
250
- if model:
251
- rows.append(Text.from_markup(f"[grey50]Model: {model}[/grey50]"))
252
- else:
253
- rows.append(
254
- Text.from_markup(
255
- "[grey50]Model:[/grey50] [yellow]not set, send /setup to configure[/yellow]"
256
- )
257
- )
272
+ for item in info_items:
273
+ rows.append(Text(f"{item.name}: {item.value}", style=item.level.value))
258
274
 
259
275
  if LATEST_VERSION_FILE.exists():
260
276
  from kimi_cli.constant import VERSION as current_version
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  import sys
3
- import termios
4
3
  import threading
5
4
  import time
6
5
  from collections.abc import AsyncGenerator, Callable
@@ -47,6 +46,21 @@ def _listen_for_keyboard_thread(
47
46
  cancel: threading.Event,
48
47
  emit: Callable[[KeyEvent], None],
49
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
+
50
64
  # make stdin raw and non-blocking
51
65
  fd = sys.stdin.fileno()
52
66
  oldterm = termios.tcgetattr(fd)
@@ -59,9 +73,9 @@ def _listen_for_keyboard_thread(
59
73
  try:
60
74
  while not cancel.is_set():
61
75
  try:
62
- c = sys.stdin.read(1)
76
+ c = sys.stdin.buffer.read(1)
63
77
  except (OSError, ValueError):
64
- c = ""
78
+ c = b""
65
79
 
66
80
  if not c:
67
81
  if cancel.is_set():
@@ -69,15 +83,15 @@ def _listen_for_keyboard_thread(
69
83
  time.sleep(0.01)
70
84
  continue
71
85
 
72
- if c == "\x1b":
86
+ if c == b"\x1b":
73
87
  sequence = c
74
88
  for _ in range(2):
75
89
  if cancel.is_set():
76
90
  break
77
91
  try:
78
- fragment = sys.stdin.read(1)
92
+ fragment = sys.stdin.buffer.read(1)
79
93
  except (OSError, ValueError):
80
- fragment = ""
94
+ fragment = b""
81
95
  if not fragment:
82
96
  break
83
97
  sequence += fragment
@@ -87,22 +101,76 @@ def _listen_for_keyboard_thread(
87
101
  event = _ARROW_KEY_MAP.get(sequence)
88
102
  if event is not None:
89
103
  emit(event)
90
- elif sequence == "\x1b":
104
+ elif sequence == b"\x1b":
91
105
  emit(KeyEvent.ESCAPE)
92
- elif c in ("\r", "\n"):
106
+ elif c in (b"\r", b"\n"):
93
107
  emit(KeyEvent.ENTER)
94
- elif c == "\t":
108
+ elif c == b"\t":
95
109
  emit(KeyEvent.TAB)
96
110
  finally:
97
111
  # restore the terminal settings
98
112
  termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
99
113
 
100
114
 
101
- _ARROW_KEY_MAP: dict[str, KeyEvent] = {
102
- "\x1b[A": KeyEvent.UP,
103
- "\x1b[B": KeyEvent.DOWN,
104
- "\x1b[C": KeyEvent.RIGHT,
105
- "\x1b[D": KeyEvent.LEFT,
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
106
174
  }
107
175
 
108
176
 
@@ -9,9 +9,9 @@ from rich.panel import Panel
9
9
 
10
10
  import kimi_cli.prompts as prompts
11
11
  from kimi_cli.soul.context import Context
12
- from kimi_cli.soul.globals import load_agents_md
13
12
  from kimi_cli.soul.kimisoul import KimiSoul
14
13
  from kimi_cli.soul.message import system
14
+ from kimi_cli.soul.runtime import load_agents_md
15
15
  from kimi_cli.ui.shell.console import console
16
16
  from kimi_cli.utils.changelog import CHANGELOG, format_release_notes
17
17
  from kimi_cli.utils.logging import logger
@@ -206,12 +206,7 @@ async def init(app: "ShellApp", args: list[str]):
206
206
  logger.info("Running `/init`")
207
207
  console.print("Analyzing the codebase...")
208
208
  tmp_context = Context(file_backend=Path(temp_dir) / "context.jsonl")
209
- app.soul = KimiSoul(
210
- soul_bak._agent,
211
- soul_bak._agent_globals,
212
- context=tmp_context,
213
- loop_control=soul_bak._loop_control,
214
- )
209
+ app.soul = KimiSoul(soul_bak._agent, soul_bak._runtime, context=tmp_context)
215
210
  ok = await app._run_soul_command(prompts.INIT)
216
211
 
217
212
  if ok:
@@ -223,7 +218,7 @@ async def init(app: "ShellApp", args: list[str]):
223
218
  console.print("[red]Failed to analyze the codebase.[/red]")
224
219
 
225
220
  app.soul = soul_bak
226
- agents_md = load_agents_md(soul_bak._agent_globals.builtin_args.KIMI_WORK_DIR)
221
+ agents_md = load_agents_md(soul_bak._runtime.builtin_args.KIMI_WORK_DIR)
227
222
  system_message = system(
228
223
  "The user just ran `/init` meta command. "
229
224
  "The system has analyzed the codebase and generated an `AGENTS.md` file. "