kimi-cli 0.42__py3-none-any.whl → 0.44__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.

@@ -1,19 +1,25 @@
1
1
  import asyncio
2
+ import base64
2
3
  import contextlib
3
4
  import getpass
4
5
  import json
5
6
  import os
6
7
  import re
8
+ import sys
7
9
  import time
8
10
  from collections.abc import Callable
9
11
  from datetime import datetime
10
12
  from enum import Enum
11
13
  from hashlib import md5
14
+ from io import BytesIO
12
15
  from pathlib import Path
13
16
  from typing import override
14
17
 
18
+ from kosong.base.message import ContentPart, ImageURLPart, TextPart
19
+ from PIL import Image, ImageGrab
15
20
  from prompt_toolkit import PromptSession
16
21
  from prompt_toolkit.application.current import get_app_or_none
22
+ from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
17
23
  from prompt_toolkit.completion import (
18
24
  Completer,
19
25
  Completion,
@@ -34,6 +40,9 @@ from kimi_cli.share import get_share_dir
34
40
  from kimi_cli.soul import StatusSnapshot
35
41
  from kimi_cli.ui.shell.metacmd import get_meta_commands
36
42
  from kimi_cli.utils.logging import logger
43
+ from kimi_cli.utils.string import random_string
44
+
45
+ PROMPT_SYMBOL = "✨"
37
46
 
38
47
 
39
48
  class MetaCommandCompleter(Completer):
@@ -360,6 +369,9 @@ class PromptMode(Enum):
360
369
  class UserInput(BaseModel):
361
370
  mode: PromptMode
362
371
  command: str
372
+ """The plain text representation of the user input."""
373
+ content: list[ContentPart]
374
+ """The rich content parts."""
363
375
 
364
376
  def __str__(self) -> str:
365
377
  return self.command
@@ -377,6 +389,11 @@ def toast(message: str, duration: float = 5.0) -> None:
377
389
  _toast_queue.put_nowait((message, duration))
378
390
 
379
391
 
392
+ _ATTACHMENT_PLACEHOLDER_RE = re.compile(
393
+ r"\[(?P<type>image):(?P<id>[a-zA-Z0-9_\-\.]+)(?:,(?P<width>\d+)x(?P<height>\d+))?\]"
394
+ )
395
+
396
+
380
397
  class CustomPromptSession:
381
398
  def __init__(self, status_provider: Callable[[], StatusSnapshot]):
382
399
  history_dir = get_share_dir() / "user-history"
@@ -386,6 +403,8 @@ class CustomPromptSession:
386
403
  self._status_provider = status_provider
387
404
  self._last_history_content: str | None = None
388
405
  self._mode: PromptMode = PromptMode.AGENT
406
+ self._attachment_parts: dict[str, ContentPart] = {}
407
+ """Mapping from attachment id to ContentPart."""
389
408
 
390
409
  history_entries = _load_history_entries(self._history_file)
391
410
  history = InMemoryHistory()
@@ -433,12 +452,20 @@ class CustomPromptSession:
433
452
  # Redraw UI
434
453
  event.app.invalidate()
435
454
 
455
+ @_kb.add("c-v", eager=True)
456
+ def _paste(event: KeyPressEvent) -> None:
457
+ if self._try_paste_image(event):
458
+ return
459
+ clipboard_data = event.app.clipboard.get_data()
460
+ event.current_buffer.paste_clipboard_data(clipboard_data)
461
+
436
462
  self._session = PromptSession(
437
463
  message=self._render_message,
438
- prompt_continuation=FormattedText([("fg:#4d4d4d", "... ")]),
464
+ # prompt_continuation=FormattedText([("fg:#4d4d4d", "... ")]),
439
465
  completer=self._agent_mode_completer,
440
466
  complete_while_typing=True,
441
467
  key_bindings=_kb,
468
+ clipboard=PyperclipClipboard(),
442
469
  history=history,
443
470
  bottom_toolbar=self._render_bottom_toolbar,
444
471
  )
@@ -448,7 +475,7 @@ class CustomPromptSession:
448
475
  self._current_toast_duration: float = 0.0
449
476
 
450
477
  def _render_message(self) -> FormattedText:
451
- symbol = "✨" if self._mode == PromptMode.AGENT else "$"
478
+ symbol = PROMPT_SYMBOL if self._mode == PromptMode.AGENT else "$"
452
479
  return FormattedText([("bold", f"{getpass.getuser()}{symbol} ")])
453
480
 
454
481
  def _apply_mode(self, event: KeyPressEvent | None = None) -> None:
@@ -501,12 +528,76 @@ class CustomPromptSession:
501
528
  if self._status_refresh_task is not None and not self._status_refresh_task.done():
502
529
  self._status_refresh_task.cancel()
503
530
  self._status_refresh_task = None
531
+ self._attachment_parts.clear()
532
+
533
+ def _try_paste_image(self, event: KeyPressEvent) -> bool:
534
+ """Try to paste an image from the clipboard. Return True if successful."""
535
+ # Try get image from clipboard
536
+ image = ImageGrab.grabclipboard()
537
+ if isinstance(image, list):
538
+ for item in image:
539
+ try:
540
+ with Image.open(item) as img:
541
+ image = img.copy()
542
+ break
543
+ except Exception:
544
+ continue
545
+ else:
546
+ image = None
547
+
548
+ if image is None:
549
+ return False
550
+
551
+ attachment_id = f"{random_string(8)}.png"
552
+ png_bytes = BytesIO()
553
+ image.save(png_bytes, format="PNG")
554
+ png_base64 = base64.b64encode(png_bytes.getvalue()).decode("ascii")
555
+ image_part = ImageURLPart(
556
+ image_url=ImageURLPart.ImageURL(
557
+ url=f"data:image/png;base64,{png_base64}", id=attachment_id
558
+ )
559
+ )
560
+ self._attachment_parts[attachment_id] = image_part
561
+ logger.debug(
562
+ "Pasted image from clipboard: {attachment_id}, {image_size}",
563
+ attachment_id=attachment_id,
564
+ image_size=image.size,
565
+ )
566
+
567
+ placeholder = f"[image:{attachment_id},{image.width}x{image.height}]"
568
+ event.current_buffer.insert_text(placeholder)
569
+ event.app.invalidate()
570
+ return True
504
571
 
505
572
  async def prompt(self) -> UserInput:
506
573
  with patch_stdout():
507
574
  command = str(await self._session.prompt_async()).strip()
575
+ command = command.replace("\x00", "") # just in case null bytes are somehow inserted
508
576
  self._append_history_entry(command)
509
- return UserInput(mode=self._mode, command=command)
577
+
578
+ # Parse rich content parts
579
+ content: list[ContentPart] = []
580
+ remaining_command = command
581
+ while match := _ATTACHMENT_PLACEHOLDER_RE.search(remaining_command):
582
+ start, end = match.span()
583
+ if start > 0:
584
+ content.append(TextPart(text=remaining_command[:start]))
585
+ attachment_id = match.group("id")
586
+ part = self._attachment_parts.get(attachment_id)
587
+ if part is not None:
588
+ content.append(part)
589
+ else:
590
+ logger.warning(
591
+ "Attachment placeholder found but no matching attachment part: {placeholder}",
592
+ placeholder=match.group(0),
593
+ )
594
+ content.append(TextPart(text=match.group(0)))
595
+ remaining_command = remaining_command[end:]
596
+
597
+ if remaining_command.strip():
598
+ content.append(TextPart(text=remaining_command.strip()))
599
+
600
+ return UserInput(mode=self._mode, content=content, command=command)
510
601
 
511
602
  def _append_history_entry(self, text: str) -> None:
512
603
  entry = _HistoryEntry(content=text.strip())
@@ -557,6 +648,7 @@ class CustomPromptSession:
557
648
  shortcuts = [
558
649
  "ctrl-x: switch mode",
559
650
  "ctrl-j: newline",
651
+ "ctrl-v: paste",
560
652
  "ctrl-d: exit",
561
653
  ]
562
654
  for shortcut in shortcuts:
@@ -579,3 +671,107 @@ class CustomPromptSession:
579
671
  def _format_status(status: StatusSnapshot) -> str:
580
672
  bounded = max(0.0, min(status.context_usage, 1.0))
581
673
  return f"context: {bounded:.1%}"
674
+
675
+
676
+ def ensure_new_line() -> None:
677
+ """Ensure the next prompt starts at column 0 regardless of prior command output."""
678
+
679
+ if not sys.stdout.isatty() or not sys.stdin.isatty():
680
+ return
681
+
682
+ needs_break = True
683
+ if sys.platform == "win32":
684
+ column = _cursor_column_windows()
685
+ needs_break = column not in (None, 0)
686
+ else:
687
+ column = _cursor_column_unix()
688
+ needs_break = column not in (None, 1)
689
+
690
+ if needs_break:
691
+ _write_newline()
692
+
693
+
694
+ def _cursor_column_unix() -> int | None:
695
+ assert sys.platform != "win32"
696
+
697
+ import select
698
+ import termios
699
+ import tty
700
+
701
+ _CURSOR_QUERY = "\x1b[6n"
702
+ _CURSOR_POSITION_RE = re.compile(r"\x1b\[(\d+);(\d+)R")
703
+
704
+ fd = sys.stdin.fileno()
705
+ oldterm = termios.tcgetattr(fd)
706
+
707
+ try:
708
+ tty.setcbreak(fd)
709
+ sys.stdout.write(_CURSOR_QUERY)
710
+ sys.stdout.flush()
711
+
712
+ response = ""
713
+ deadline = time.monotonic() + 0.2
714
+ while time.monotonic() < deadline:
715
+ timeout = max(0.01, deadline - time.monotonic())
716
+ ready, _, _ = select.select([sys.stdin], [], [], timeout)
717
+ if not ready:
718
+ continue
719
+ try:
720
+ chunk = os.read(fd, 32)
721
+ except OSError:
722
+ break
723
+ if not chunk:
724
+ break
725
+ response += chunk.decode(errors="ignore")
726
+ match = _CURSOR_POSITION_RE.search(response)
727
+ if match:
728
+ return int(match.group(2))
729
+ finally:
730
+ termios.tcsetattr(fd, termios.TCSADRAIN, oldterm)
731
+
732
+ return None
733
+
734
+
735
+ def _cursor_column_windows() -> int | None:
736
+ assert sys.platform == "win32"
737
+
738
+ import ctypes
739
+ from ctypes import wintypes
740
+
741
+ kernel32 = ctypes.windll.kernel32
742
+ _STD_OUTPUT_HANDLE = -11 # Windows API constant for standard output handle
743
+ handle = kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
744
+ invalid_handle_value = ctypes.c_void_p(-1).value
745
+ if handle in (0, invalid_handle_value):
746
+ return None
747
+
748
+ class COORD(ctypes.Structure):
749
+ _fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)]
750
+
751
+ class SMALL_RECT(ctypes.Structure):
752
+ _fields_ = [
753
+ ("Left", wintypes.SHORT),
754
+ ("Top", wintypes.SHORT),
755
+ ("Right", wintypes.SHORT),
756
+ ("Bottom", wintypes.SHORT),
757
+ ]
758
+
759
+ class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
760
+ _fields_ = [
761
+ ("dwSize", COORD),
762
+ ("dwCursorPosition", COORD),
763
+ ("wAttributes", wintypes.WORD),
764
+ ("srWindow", SMALL_RECT),
765
+ ("dwMaximumWindowSize", COORD),
766
+ ]
767
+
768
+ csbi = CONSOLE_SCREEN_BUFFER_INFO()
769
+ if not kernel32.GetConsoleScreenBufferInfo(handle, ctypes.byref(csbi)):
770
+ return None
771
+
772
+ return int(csbi.dwCursorPosition.X)
773
+
774
+
775
+ def _write_newline() -> None:
776
+ sys.stdout.write("\n")
777
+ sys.stdout.flush()
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ import contextlib
3
+ import getpass
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+
7
+ from kosong.base.message import Message, TextPart
8
+ from kosong.tooling import ToolError, ToolOk
9
+
10
+ from kimi_cli.soul import StatusSnapshot
11
+ from kimi_cli.ui.shell.console import console
12
+ from kimi_cli.ui.shell.prompt import PROMPT_SYMBOL
13
+ from kimi_cli.ui.shell.visualize import visualize
14
+ from kimi_cli.utils.message import message_extract_text, message_stringify
15
+ from kimi_cli.wire import Wire
16
+ from kimi_cli.wire.message import ContentPart, StepBegin, ToolCall, ToolResult
17
+
18
+ MAX_REPLAY_RUNS = 5
19
+
20
+ type _ReplayEvent = StepBegin | ToolCall | ContentPart | ToolResult
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class _ReplayRun:
25
+ user_message: Message
26
+ events: list[_ReplayEvent]
27
+ n_steps: int = 0
28
+
29
+
30
+ async def replay_recent_history(history: Sequence[Message]) -> None:
31
+ """
32
+ Replay the most recent user-initiated runs from the provided message history.
33
+ """
34
+ start_idx = _find_replay_start(history)
35
+ if start_idx is None:
36
+ return
37
+
38
+ runs = _build_replay_runs(history[start_idx:])
39
+ if not runs:
40
+ return
41
+
42
+ for run in runs:
43
+ wire = Wire()
44
+ console.print(f"{getpass.getuser()}{PROMPT_SYMBOL} {message_stringify(run.user_message)}")
45
+ ui_task = asyncio.create_task(
46
+ visualize(wire.ui_side, initial_status=StatusSnapshot(context_usage=0.0))
47
+ )
48
+ for event in run.events:
49
+ wire.soul_side.send(event)
50
+ await asyncio.sleep(0) # yield to UI loop
51
+ wire.shutdown()
52
+ with contextlib.suppress(asyncio.QueueShutDown):
53
+ await ui_task
54
+
55
+
56
+ def _is_user_message(message: Message) -> bool:
57
+ # FIXME: should consider non-text tool call results which are sent as user messages
58
+ if message.role != "user":
59
+ return False
60
+ return not message_extract_text(message).startswith("<system>CHECKPOINT")
61
+
62
+
63
+ def _find_replay_start(history: Sequence[Message]) -> int | None:
64
+ indices = [idx for idx, message in enumerate(history) if _is_user_message(message)]
65
+ if not indices:
66
+ return None
67
+ # only replay last MAX_REPLAY_RUNS messages
68
+ return indices[max(0, len(indices) - MAX_REPLAY_RUNS)]
69
+
70
+
71
+ def _build_replay_runs(history: Sequence[Message]) -> list[_ReplayRun]:
72
+ runs: list[_ReplayRun] = []
73
+ current_run: _ReplayRun | None = None
74
+ for message in history:
75
+ if _is_user_message(message):
76
+ # start a new run
77
+ if current_run is not None:
78
+ runs.append(current_run)
79
+ current_run = _ReplayRun(user_message=message, events=[])
80
+ elif message.role == "assistant":
81
+ if current_run is None:
82
+ continue
83
+ current_run.n_steps += 1
84
+ current_run.events.append(StepBegin(n=current_run.n_steps))
85
+ if isinstance(message.content, str):
86
+ current_run.events.append(TextPart(text=message.content))
87
+ else:
88
+ current_run.events.extend(message.content)
89
+ current_run.events.extend(message.tool_calls or [])
90
+ elif message.role == "tool":
91
+ if current_run is None:
92
+ continue
93
+ assert message.tool_call_id is not None
94
+ if isinstance(message.content, list) and any(
95
+ isinstance(part, TextPart) and part.text.startswith("<system>ERROR")
96
+ for part in message.content
97
+ ):
98
+ result = ToolError(message="", output="", brief="")
99
+ else:
100
+ result = ToolOk(output=message.content)
101
+ current_run.events.append(ToolResult(tool_call_id=message.tool_call_id, result=result))
102
+ if current_run is not None:
103
+ runs.append(current_run)
104
+ return runs
@@ -26,7 +26,7 @@ class _Platform(NamedTuple):
26
26
  _PLATFORMS = [
27
27
  _Platform(
28
28
  id="kimi-for-coding",
29
- name="Kimi For Coding (CN)",
29
+ name="Kimi For Coding",
30
30
  base_url="https://api.kimi.com/coding/v1",
31
31
  search_url="https://api.kimi.com/coding/v1/search",
32
32
  ),
@@ -8,7 +8,6 @@ from kimi_cli.soul import StatusSnapshot
8
8
  from kimi_cli.ui.shell.console import console
9
9
  from kimi_cli.ui.shell.keyboard import listen_for_keyboard
10
10
  from kimi_cli.ui.shell.liveview import StepLiveView, StepLiveViewWithMarkdown
11
- from kimi_cli.utils.logging import logger
12
11
  from kimi_cli.wire import WireUISide
13
12
  from kimi_cli.wire.message import (
14
13
  ApprovalRequest,
@@ -46,69 +45,67 @@ async def visualize(
46
45
  ):
47
46
  """
48
47
  A loop to consume agent events and visualize the agent behavior.
49
- This loop never raise any exception except asyncio.CancelledError.
50
48
 
51
49
  Args:
52
50
  wire: Communication channel with the agent
53
51
  initial_status: Initial status snapshot
54
52
  cancel_event: Event that can be set (e.g., by ESC key) to cancel the run
55
53
  """
54
+
56
55
  latest_status = initial_status
57
- try:
58
- # expect a StepBegin
59
- assert isinstance(await wire.receive(), StepBegin)
60
-
61
- while True:
62
- # TODO: Maybe we can always have a StepLiveView here.
63
- # No need to recreate for each step.
64
- with StepLiveViewWithMarkdown(latest_status, cancel_event) as step:
65
- async with _keyboard_listener(step):
66
- # spin the moon at the beginning of each step
67
- with console.status("", spinner="moon"):
68
- msg = await wire.receive()
69
56
 
70
- if isinstance(msg, CompactionBegin):
71
- with console.status("[cyan]Compacting...[/cyan]"):
72
- msg = await wire.receive()
73
- if isinstance(msg, StepInterrupted):
74
- break
75
- assert isinstance(msg, CompactionEnd)
76
- continue
77
-
78
- # visualization loop for one step
79
- while True:
80
- match msg:
81
- case TextPart(text=text):
82
- step.append_text(text)
83
- case ContentPart():
84
- # TODO: support more content parts
85
- step.append_text(f"[{msg.__class__.__name__}]")
86
- case ToolCall():
87
- step.append_tool_call(msg)
88
- case ToolCallPart():
89
- step.append_tool_call_part(msg)
90
- case ToolResult():
91
- step.append_tool_result(msg)
92
- case ApprovalRequest():
93
- step.request_approval(msg)
94
- case StatusUpdate(status=status):
95
- latest_status = status
96
- step.update_status(latest_status)
97
- case _:
98
- break # break the step loop
99
- msg = await wire.receive()
57
+ # expect a StepBegin
58
+ assert isinstance(await wire.receive(), StepBegin)
59
+
60
+ while True:
61
+ # TODO: Maybe we can always have a StepLiveView here.
62
+ # No need to recreate for each step.
63
+ with StepLiveViewWithMarkdown(latest_status, cancel_event) as step:
64
+ async with _keyboard_listener(step):
65
+ # spin the moon at the beginning of each step
66
+ with console.status("", spinner="moon"):
67
+ msg = await wire.receive()
100
68
 
101
- # cleanup the step live view
69
+ if isinstance(msg, CompactionBegin):
70
+ with console.status("[cyan]Compacting...[/cyan]"):
71
+ msg = await wire.receive()
102
72
  if isinstance(msg, StepInterrupted):
103
- step.interrupt()
104
- else:
105
- step.finish()
106
-
107
- if isinstance(msg, StepInterrupted):
108
- # for StepInterrupted, the visualization loop should end immediately
109
- break
110
-
111
- assert isinstance(msg, StepBegin), "expect a StepBegin"
112
- # start a new step
113
- except asyncio.QueueShutDown:
114
- logger.debug("Visualization loop shutting down")
73
+ break
74
+ assert isinstance(msg, CompactionEnd)
75
+ continue
76
+
77
+ # visualization loop for one step
78
+ while True:
79
+ match msg:
80
+ case TextPart(text=text):
81
+ step.append_text(text)
82
+ case ContentPart():
83
+ # TODO: support more content parts
84
+ step.append_text(f"[{msg.__class__.__name__}]")
85
+ case ToolCall():
86
+ step.append_tool_call(msg)
87
+ case ToolCallPart():
88
+ step.append_tool_call_part(msg)
89
+ case ToolResult():
90
+ step.append_tool_result(msg)
91
+ case ApprovalRequest():
92
+ step.request_approval(msg)
93
+ case StatusUpdate(status=status):
94
+ latest_status = status
95
+ step.update_status(latest_status)
96
+ case _:
97
+ break # break the step loop
98
+ msg = await wire.receive()
99
+
100
+ # cleanup the step live view
101
+ if isinstance(msg, StepInterrupted):
102
+ step.interrupt()
103
+ else:
104
+ step.finish()
105
+
106
+ if isinstance(msg, StepInterrupted):
107
+ # for StepInterrupted, the visualization loop should end immediately
108
+ break
109
+
110
+ assert isinstance(msg, StepBegin), "expect a StepBegin"
111
+ # start a new step
kimi_cli/utils/message.py CHANGED
@@ -6,3 +6,17 @@ def message_extract_text(message: Message) -> str:
6
6
  if isinstance(message.content, str):
7
7
  return message.content
8
8
  return "\n".join(part.text for part in message.content if isinstance(part, TextPart))
9
+
10
+
11
+ def message_stringify(message: Message) -> str:
12
+ """Get a string representation of a message."""
13
+ parts: list[str] = []
14
+ if isinstance(message.content, str):
15
+ parts.append(message.content)
16
+ else:
17
+ for part in message.content:
18
+ if isinstance(part, TextPart):
19
+ parts.append(part.text)
20
+ else:
21
+ parts.append(f"[{part.type}]")
22
+ return "".join(parts)
@@ -0,0 +1,41 @@
1
+ import asyncio
2
+ import contextlib
3
+ import signal
4
+ from collections.abc import Callable
5
+
6
+
7
+ def install_sigint_handler(
8
+ loop: asyncio.AbstractEventLoop, handler: Callable[[], None]
9
+ ) -> Callable[[], None]:
10
+ """
11
+ Install a SIGINT handler that works on Unix and Windows.
12
+
13
+ On Unix event loops, prefer `loop.add_signal_handler`.
14
+ On Windows (or other platforms) where it is not implemented, fall back to
15
+ `signal.signal`. The fallback cannot be removed from the loop, but we
16
+ restore the previous handler on uninstall.
17
+
18
+ Returns:
19
+ A function that removes the installed handler. It is guaranteed that
20
+ no exceptions are raised when calling the returned function.
21
+ """
22
+
23
+ try:
24
+ loop.add_signal_handler(signal.SIGINT, handler)
25
+
26
+ def remove() -> None:
27
+ with contextlib.suppress(RuntimeError):
28
+ loop.remove_signal_handler(signal.SIGINT)
29
+
30
+ return remove
31
+ except RuntimeError:
32
+ # Windows ProactorEventLoop and some environments do not support
33
+ # add_signal_handler. Use synchronous signal handling as a fallback.
34
+ previous = signal.getsignal(signal.SIGINT)
35
+ signal.signal(signal.SIGINT, lambda signum, frame: handler())
36
+
37
+ def remove() -> None:
38
+ with contextlib.suppress(RuntimeError):
39
+ signal.signal(signal.SIGINT, previous)
40
+
41
+ return remove
kimi_cli/utils/string.py CHANGED
@@ -1,4 +1,6 @@
1
+ import random
1
2
  import re
3
+ import string
2
4
 
3
5
  _NEWLINE_RE = re.compile(r"[\r\n]+")
4
6
 
@@ -10,3 +12,9 @@ def shorten_middle(text: str, width: int, remove_newline: bool = True) -> str:
10
12
  if remove_newline:
11
13
  text = _NEWLINE_RE.sub(" ", text)
12
14
  return text[: width // 2] + "..." + text[-width // 2 :]
15
+
16
+
17
+ def random_string(length: int = 8) -> str:
18
+ """Generate a random string of fixed length."""
19
+ letters = string.ascii_lowercase
20
+ return "".join(random.choice(letters) for _ in range(length))
kimi_cli/wire/__init__.py CHANGED
@@ -25,6 +25,7 @@ class Wire:
25
25
  return self._ui_side
26
26
 
27
27
  def shutdown(self) -> None:
28
+ logger.debug("Shutting down wire")
28
29
  self._queue.shutdown()
29
30
 
30
31
 
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kimi-cli
3
- Version: 0.42
3
+ Version: 0.44
4
4
  Summary: Kimi CLI is your next CLI agent.
5
- Requires-Dist: agent-client-protocol==0.4.9
5
+ Requires-Dist: agent-client-protocol==0.6.2
6
6
  Requires-Dist: aiofiles==25.1.0
7
- Requires-Dist: aiohttp==3.13.1
7
+ Requires-Dist: aiohttp==3.13.2
8
8
  Requires-Dist: click==8.3.0
9
- Requires-Dist: kosong==0.15.0
9
+ Requires-Dist: kosong==0.16.1
10
10
  Requires-Dist: loguru==0.7.3
11
11
  Requires-Dist: patch-ng==1.19.0
12
12
  Requires-Dist: prompt-toolkit==3.0.52
13
+ Requires-Dist: pillow==12.0.0
13
14
  Requires-Dist: pyyaml==6.0.3
14
15
  Requires-Dist: rich==14.2.0
15
16
  Requires-Dist: ripgrepy==2.2.0
@@ -84,7 +85,7 @@ After setup, Kimi CLI will be ready to use. You can send `/help` to get more inf
84
85
 
85
86
  ### Shell mode
86
87
 
87
- Kimi CLI is not only a coding agent, but also a shell. You can switch the mode by pressing `Ctrl-K`. In shell mode, you can directly run shell commands without leaving Kimi CLI.
88
+ Kimi CLI is not only a coding agent, but also a shell. You can switch the mode by pressing `Ctrl-X`. In shell mode, you can directly run shell commands without leaving Kimi CLI.
88
89
 
89
90
  > [!NOTE]
90
91
  > Built-in shell commands like `cd` are not supported yet.
@@ -109,7 +110,7 @@ Then add `kimi-cli` to your Zsh plugin list in `~/.zshrc`:
109
110
  plugins=(... kimi-cli)
110
111
  ```
111
112
 
112
- After restarting Zsh, you can switch to agent mode by pressing `Ctrl-K`.
113
+ After restarting Zsh, you can switch to agent mode by pressing `Ctrl-X`.
113
114
 
114
115
  ### ACP support
115
116