kimi-cli 0.39__py3-none-any.whl → 0.41__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 (52) hide show
  1. kimi_cli/CHANGELOG.md +23 -0
  2. kimi_cli/__init__.py +18 -280
  3. kimi_cli/agents/koder/system.md +1 -1
  4. kimi_cli/agentspec.py +104 -0
  5. kimi_cli/cli.py +235 -0
  6. kimi_cli/constant.py +4 -0
  7. kimi_cli/llm.py +69 -0
  8. kimi_cli/prompts/__init__.py +2 -2
  9. kimi_cli/soul/__init__.py +102 -6
  10. kimi_cli/soul/agent.py +157 -0
  11. kimi_cli/soul/approval.py +2 -4
  12. kimi_cli/soul/compaction.py +10 -10
  13. kimi_cli/soul/context.py +5 -0
  14. kimi_cli/soul/globals.py +92 -0
  15. kimi_cli/soul/kimisoul.py +26 -30
  16. kimi_cli/soul/message.py +5 -5
  17. kimi_cli/tools/bash/__init__.py +2 -2
  18. kimi_cli/tools/dmail/__init__.py +1 -1
  19. kimi_cli/tools/file/glob.md +1 -1
  20. kimi_cli/tools/file/glob.py +2 -2
  21. kimi_cli/tools/file/grep.py +3 -2
  22. kimi_cli/tools/file/patch.py +2 -2
  23. kimi_cli/tools/file/read.py +1 -1
  24. kimi_cli/tools/file/replace.py +2 -2
  25. kimi_cli/tools/file/write.py +2 -2
  26. kimi_cli/tools/task/__init__.py +23 -22
  27. kimi_cli/tools/task/task.md +1 -1
  28. kimi_cli/tools/todo/__init__.py +1 -1
  29. kimi_cli/tools/utils.py +1 -1
  30. kimi_cli/tools/web/fetch.py +2 -1
  31. kimi_cli/tools/web/search.py +4 -4
  32. kimi_cli/ui/__init__.py +0 -69
  33. kimi_cli/ui/acp/__init__.py +8 -9
  34. kimi_cli/ui/print/__init__.py +17 -35
  35. kimi_cli/ui/shell/__init__.py +9 -15
  36. kimi_cli/ui/shell/liveview.py +13 -4
  37. kimi_cli/ui/shell/metacmd.py +3 -3
  38. kimi_cli/ui/shell/setup.py +7 -6
  39. kimi_cli/ui/shell/update.py +4 -3
  40. kimi_cli/ui/shell/visualize.py +18 -9
  41. kimi_cli/utils/aiohttp.py +10 -0
  42. kimi_cli/utils/changelog.py +3 -1
  43. kimi_cli/wire/__init__.py +57 -0
  44. kimi_cli/{soul/wire.py → wire/message.py} +4 -39
  45. {kimi_cli-0.39.dist-info → kimi_cli-0.41.dist-info}/METADATA +35 -2
  46. kimi_cli-0.41.dist-info/RECORD +85 -0
  47. kimi_cli-0.41.dist-info/entry_points.txt +3 -0
  48. kimi_cli/agent.py +0 -261
  49. kimi_cli/utils/provider.py +0 -72
  50. kimi_cli-0.39.dist-info/RECORD +0 -80
  51. kimi_cli-0.39.dist-info/entry_points.txt +0 -3
  52. {kimi_cli-0.39.dist-info → kimi_cli-0.41.dist-info}/WHEEL +0 -0
@@ -12,18 +12,17 @@ from kosong.base.message import (
12
12
  from kosong.chat_provider import ChatProviderError
13
13
  from kosong.tooling import ToolError, ToolOk, ToolResult
14
14
 
15
- from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
16
- from kimi_cli.soul.wire import (
15
+ from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
16
+ from kimi_cli.tools import extract_subtitle
17
+ from kimi_cli.utils.logging import logger
18
+ from kimi_cli.wire import WireUISide
19
+ from kimi_cli.wire.message import (
17
20
  ApprovalRequest,
18
21
  ApprovalResponse,
19
22
  StatusUpdate,
20
23
  StepBegin,
21
24
  StepInterrupted,
22
- Wire,
23
25
  )
24
- from kimi_cli.tools import extract_subtitle
25
- from kimi_cli.ui import RunCancelled, run_soul
26
- from kimi_cli.utils.logging import logger
27
26
 
28
27
 
29
28
  class _ToolCallState:
@@ -64,7 +63,7 @@ class _RunState:
64
63
  self.cancel_event = asyncio.Event()
65
64
 
66
65
 
67
- class ACPAgentImpl:
66
+ class ACPAgent:
68
67
  """Implementation of the ACP Agent protocol."""
69
68
 
70
69
  def __init__(self, soul: Soul, connection: acp.AgentSideConnection):
@@ -172,7 +171,7 @@ class ACPAgentImpl:
172
171
  logger.info("Cancelling running prompt")
173
172
  self.run_state.cancel_event.set()
174
173
 
175
- async def _stream_events(self, wire: Wire):
174
+ async def _stream_events(self, wire: WireUISide):
176
175
  try:
177
176
  # expect a StepBegin
178
177
  assert isinstance(await wire.receive(), StepBegin)
@@ -428,7 +427,7 @@ class ACPServer:
428
427
 
429
428
  # Create connection - the library handles all JSON-RPC details!
430
429
  _ = acp.AgentSideConnection(
431
- lambda conn: ACPAgentImpl(self.soul, conn),
430
+ lambda conn: ACPAgent(self.soul, conn),
432
431
  writer,
433
432
  reader,
434
433
  )
@@ -3,18 +3,18 @@ import json
3
3
  import signal
4
4
  import sys
5
5
  from functools import partial
6
+ from pathlib import Path
6
7
  from typing import Literal
7
8
 
8
9
  import aiofiles
9
10
  from kosong.base.message import Message
10
11
  from kosong.chat_provider import ChatProviderError
11
12
 
12
- from kimi_cli.soul import LLMNotSet, MaxStepsReached
13
- from kimi_cli.soul.kimisoul import KimiSoul
14
- from kimi_cli.soul.wire import StepInterrupted, Wire
15
- from kimi_cli.ui import RunCancelled, run_soul
13
+ from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
16
14
  from kimi_cli.utils.logging import logger
17
15
  from kimi_cli.utils.message import message_extract_text
16
+ from kimi_cli.wire import WireUISide
17
+ from kimi_cli.wire.message import StepInterrupted
18
18
 
19
19
  InputFormat = Literal["text", "stream-json"]
20
20
  OutputFormat = Literal["text", "stream-json"]
@@ -25,17 +25,23 @@ class PrintApp:
25
25
  An app implementation that prints the agent behavior to the console.
26
26
 
27
27
  Args:
28
- soul (KimiSoul): The soul to run. Only `KimiSoul` is supported.
28
+ soul (Soul): The soul to run.
29
29
  input_format (InputFormat): The input format to use.
30
30
  output_format (OutputFormat): The output format to use.
31
+ context_file (Path): The file to store the context.
31
32
  """
32
33
 
33
- def __init__(self, soul: KimiSoul, input_format: InputFormat, output_format: OutputFormat):
34
+ def __init__(
35
+ self,
36
+ soul: Soul,
37
+ input_format: InputFormat,
38
+ output_format: OutputFormat,
39
+ context_file: Path,
40
+ ):
34
41
  self.soul = soul
35
42
  self.input_format = input_format
36
43
  self.output_format = output_format
37
- self.soul._approval.set_yolo(True)
38
- # TODO(approval): proper approval request handling
44
+ self.context_file = context_file
39
45
 
40
46
  async def run(self, command: str | None = None) -> bool:
41
47
  cancel_event = asyncio.Event()
@@ -95,30 +101,6 @@ class PrintApp:
95
101
  loop.remove_signal_handler(signal.SIGINT)
96
102
  return False
97
103
 
98
- # TODO: unify with `_soul_run` in `ShellApp` and `ACPAgentImpl`
99
- async def _soul_run(self, user_input: str):
100
- wire = Wire()
101
- logger.debug("Starting visualization loop")
102
-
103
- if self.output_format == "text":
104
- vis_task = asyncio.create_task(self._visualize_text(wire))
105
- else:
106
- assert self.output_format == "stream-json"
107
- if not self.soul._context._file_backend.exists():
108
- self.soul._context._file_backend.touch()
109
- start_position = self.soul._context._file_backend.stat().st_size
110
- vis_task = asyncio.create_task(self._visualize_stream_json(wire, start_position))
111
-
112
- try:
113
- await self.soul.run(user_input, wire)
114
- finally:
115
- wire.shutdown()
116
- # shutting down the event queue should break the visualization loop
117
- try:
118
- await asyncio.wait_for(vis_task, timeout=0.5)
119
- except TimeoutError:
120
- logger.warning("Visualization loop timed out")
121
-
122
104
  def _read_next_command(self) -> str | None:
123
105
  while True:
124
106
  json_line = sys.stdin.readline()
@@ -144,7 +126,7 @@ class PrintApp:
144
126
  except Exception:
145
127
  logger.warning("Ignoring invalid user message: {json_line}", json_line=json_line)
146
128
 
147
- async def _visualize_text(self, wire: Wire):
129
+ async def _visualize_text(self, wire: WireUISide):
148
130
  try:
149
131
  while True:
150
132
  msg = await wire.receive()
@@ -154,10 +136,10 @@ class PrintApp:
154
136
  except asyncio.QueueShutDown:
155
137
  logger.debug("Visualization loop shutting down")
156
138
 
157
- async def _visualize_stream_json(self, wire: Wire, start_position: int):
139
+ async def _visualize_stream_json(self, wire: WireUISide, start_position: int):
158
140
  # TODO: be aware of context compaction
159
141
  try:
160
- async with aiofiles.open(self.soul._context._file_backend, encoding="utf-8") as f:
142
+ async with aiofiles.open(self.context_file, encoding="utf-8") as f:
161
143
  await f.seek(start_position)
162
144
  while True:
163
145
  should_end = False
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import signal
3
3
  from collections.abc import Awaitable, Coroutine
4
- from functools import partial
5
4
  from typing import Any
6
5
 
7
6
  from kosong.chat_provider import APIStatusError, ChatProviderError
@@ -10,9 +9,8 @@ from rich.panel import Panel
10
9
  from rich.table import Table
11
10
  from rich.text import Text
12
11
 
13
- from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
12
+ from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
14
13
  from kimi_cli.soul.kimisoul import KimiSoul
15
- from kimi_cli.ui import RunCancelled, run_soul
16
14
  from kimi_cli.ui.shell.console import console
17
15
  from kimi_cli.ui.shell.metacmd import get_meta_command
18
16
  from kimi_cli.ui.shell.prompt import CustomPromptSession, PromptMode, toast
@@ -21,12 +19,6 @@ from kimi_cli.ui.shell.visualize import visualize
21
19
  from kimi_cli.utils.logging import logger
22
20
 
23
21
 
24
- class Reload(Exception):
25
- """Reload configuration."""
26
-
27
- pass
28
-
29
-
30
22
  class ShellApp:
31
23
  def __init__(self, soul: Soul, welcome_info: dict[str, str] | None = None):
32
24
  self.soul = soul
@@ -107,6 +99,8 @@ class ShellApp:
107
99
  loop.remove_signal_handler(signal.SIGINT)
108
100
 
109
101
  async def _run_meta_command(self, command_str: str):
102
+ from kimi_cli.cli import Reload
103
+
110
104
  parts = command_str.split(" ")
111
105
  command_name = parts[0]
112
106
  command_args = parts[1:]
@@ -160,10 +154,13 @@ class ShellApp:
160
154
  loop.add_signal_handler(signal.SIGINT, _handler)
161
155
 
162
156
  try:
157
+ # Use lambda to pass cancel_event via closure
163
158
  await run_soul(
164
159
  self.soul,
165
160
  command,
166
- partial(visualize, initial_status=self.soul.status),
161
+ lambda wire: visualize(
162
+ wire, initial_status=self.soul.status, cancel_event=cancel_event
163
+ ),
167
164
  cancel_event,
168
165
  )
169
166
  return True
@@ -186,9 +183,6 @@ class ShellApp:
186
183
  except RunCancelled:
187
184
  logger.info("Cancelled by user")
188
185
  console.print("[red]Interrupted by user[/red]")
189
- except Reload:
190
- # just propagate
191
- raise
192
186
  except BaseException as e:
193
187
  logger.exception("Unknown error:")
194
188
  console.print(f"[red]Unknown error: {e}[/red]")
@@ -263,9 +257,9 @@ def _print_welcome_info(name: str, model: str, info_items: dict[str, str]) -> No
263
257
  )
264
258
 
265
259
  if LATEST_VERSION_FILE.exists():
266
- from kimi_cli import __version__ as current_version
260
+ from kimi_cli.constant import VERSION as current_version
267
261
 
268
- latest_version = LATEST_VERSION_FILE.read_text().strip()
262
+ latest_version = LATEST_VERSION_FILE.read_text(encoding="utf-8").strip()
269
263
  if semver_tuple(latest_version) > semver_tuple(current_version):
270
264
  rows.append(
271
265
  Text.from_markup(
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from collections import deque
2
3
 
3
4
  import streamingjson
@@ -14,10 +15,10 @@ from rich.status import Status
14
15
  from rich.text import Text
15
16
 
16
17
  from kimi_cli.soul import StatusSnapshot
17
- from kimi_cli.soul.wire import ApprovalRequest, ApprovalResponse
18
18
  from kimi_cli.tools import extract_subtitle
19
19
  from kimi_cli.ui.shell.console import console
20
20
  from kimi_cli.ui.shell.keyboard import KeyEvent
21
+ from kimi_cli.wire.message import ApprovalRequest, ApprovalResponse
21
22
 
22
23
 
23
24
  class _ToolCallDisplay:
@@ -125,7 +126,7 @@ class _ApprovalRequestDisplay:
125
126
 
126
127
 
127
128
  class StepLiveView:
128
- def __init__(self, status: StatusSnapshot):
129
+ def __init__(self, status: StatusSnapshot, cancel_event: asyncio.Event | None = None):
129
130
  # message content
130
131
  self._line_buffer = Text("")
131
132
 
@@ -144,6 +145,9 @@ class StepLiveView:
144
145
  )
145
146
  self._buffer_status: RenderableType | None = None
146
147
 
148
+ # cancel event for ESC key handling
149
+ self._cancel_event = cancel_event
150
+
147
151
  def __enter__(self):
148
152
  self._live = Live(
149
153
  self._compose(),
@@ -243,6 +247,11 @@ class StepLiveView:
243
247
  self._status_text.plain = self._format_status(status)
244
248
 
245
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
+
246
255
  if not self._current_approval:
247
256
  # just ignore any keyboard event when there's no approval request
248
257
  return
@@ -298,8 +307,8 @@ class StepLiveView:
298
307
  class StepLiveViewWithMarkdown(StepLiveView):
299
308
  # TODO: figure out a streaming implementation for this
300
309
 
301
- def __init__(self, status: StatusSnapshot):
302
- super().__init__(status)
310
+ def __init__(self, status: StatusSnapshot, cancel_event: asyncio.Event | None = None):
311
+ super().__init__(status, cancel_event)
303
312
  self._pending_markdown_parts: list[str] = []
304
313
  self._buffer_status_active = False
305
314
  self._buffer_status_obj: Status | None = None
@@ -8,8 +8,8 @@ from kosong.base.message import Message
8
8
  from rich.panel import Panel
9
9
 
10
10
  import kimi_cli.prompts as prompts
11
- from kimi_cli.agent import load_agents_md
12
11
  from kimi_cli.soul.context import Context
12
+ from kimi_cli.soul.globals import load_agents_md
13
13
  from kimi_cli.soul.kimisoul import KimiSoul
14
14
  from kimi_cli.soul.message import system
15
15
  from kimi_cli.ui.shell.console import console
@@ -173,9 +173,9 @@ def help(app: "ShellApp", args: list[str]):
173
173
  @meta_command
174
174
  def version(app: "ShellApp", args: list[str]):
175
175
  """Show version information"""
176
- from kimi_cli import __version__
176
+ from kimi_cli.constant import VERSION
177
177
 
178
- console.print(f"kimi, version {__version__}")
178
+ console.print(f"kimi, version {VERSION}")
179
179
 
180
180
 
181
181
  @meta_command(name="release-notes")
@@ -9,6 +9,7 @@ from pydantic import SecretStr
9
9
  from kimi_cli.config import LLMModel, LLMProvider, MoonshotSearchConfig, load_config, save_config
10
10
  from kimi_cli.ui.shell.console import console
11
11
  from kimi_cli.ui.shell.metacmd import meta_command
12
+ from kimi_cli.utils.aiohttp import new_client_session
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from kimi_cli.ui.shell import ShellApp
@@ -25,19 +26,19 @@ class _Platform(NamedTuple):
25
26
  _PLATFORMS = [
26
27
  _Platform(
27
28
  id="kimi-for-coding",
28
- name="Kimi For Coding",
29
+ name="Kimi For Coding (CN)",
29
30
  base_url="https://api.kimi.com/coding/v1",
30
31
  search_url="https://api.kimi.com/coding/v1/search",
31
32
  ),
32
33
  _Platform(
33
34
  id="moonshot-cn",
34
- name="Moonshot AI 开放平台",
35
+ name="Moonshot AI 开放平台 (moonshot.cn)",
35
36
  base_url="https://api.moonshot.cn/v1",
36
37
  allowed_models=["kimi-k2-turbo-preview", "kimi-k2-0905-preview", "kimi-k2-0711-preview"],
37
38
  ),
38
39
  _Platform(
39
40
  id="moonshot-ai",
40
- name="Moonshot AI Open Platform",
41
+ name="Moonshot AI Open Platform (moonshot.ai)",
41
42
  base_url="https://api.moonshot.ai/v1",
42
43
  allowed_models=["kimi-k2-turbo-preview", "kimi-k2-0905-preview", "kimi-k2-0711-preview"],
43
44
  ),
@@ -76,7 +77,7 @@ async def setup(app: "ShellApp", args: list[str]):
76
77
  await asyncio.sleep(1)
77
78
  console.clear()
78
79
 
79
- from kimi_cli import Reload
80
+ from kimi_cli.cli import Reload
80
81
 
81
82
  raise Reload
82
83
 
@@ -109,7 +110,7 @@ async def _setup() -> _SetupResult | None:
109
110
  models_url = f"{platform.base_url}/models"
110
111
  try:
111
112
  async with (
112
- aiohttp.ClientSession() as session,
113
+ new_client_session() as session,
113
114
  session.get(
114
115
  models_url,
115
116
  headers={
@@ -184,6 +185,6 @@ async def _prompt_text(prompt: str, *, is_password: bool = False) -> str | None:
184
185
  @meta_command
185
186
  def reload(app: "ShellApp", args: list[str]):
186
187
  """Reload configuration"""
187
- from kimi_cli import Reload
188
+ from kimi_cli.cli import Reload
188
189
 
189
190
  raise Reload
@@ -13,6 +13,7 @@ import aiohttp
13
13
 
14
14
  from kimi_cli.share import get_share_dir
15
15
  from kimi_cli.ui.shell.console import console
16
+ from kimi_cli.utils.aiohttp import new_client_session
16
17
  from kimi_cli.utils.logging import logger
17
18
 
18
19
  BASE_URL = "https://cdn.kimi.com/binaries/kimi-cli"
@@ -84,7 +85,7 @@ LATEST_VERSION_FILE = get_share_dir() / "latest_version.txt"
84
85
 
85
86
 
86
87
  async def _do_update(*, print: bool, check_only: bool) -> UpdateResult:
87
- from kimi_cli import __version__ as current_version
88
+ from kimi_cli.constant import VERSION as current_version
88
89
 
89
90
  def _print(message: str) -> None:
90
91
  if print:
@@ -95,7 +96,7 @@ async def _do_update(*, print: bool, check_only: bool) -> UpdateResult:
95
96
  _print("[red]Failed to detect target platform.[/red]")
96
97
  return UpdateResult.UNSUPPORTED
97
98
 
98
- async with aiohttp.ClientSession() as session:
99
+ async with new_client_session() as session:
99
100
  logger.info("Checking for updates...")
100
101
  _print("Checking for updates...")
101
102
  latest_version = await _get_latest_version(session)
@@ -104,7 +105,7 @@ async def _do_update(*, print: bool, check_only: bool) -> UpdateResult:
104
105
  return UpdateResult.FAILED
105
106
 
106
107
  logger.debug("Latest version: {latest_version}", latest_version=latest_version)
107
- LATEST_VERSION_FILE.write_text(latest_version)
108
+ LATEST_VERSION_FILE.write_text(latest_version, encoding="utf-8")
108
109
 
109
110
  cur_t = semver_tuple(current_version)
110
111
  lat_t = semver_tuple(latest_version)
@@ -5,19 +5,19 @@ from kosong.base.message import ContentPart, TextPart, ToolCall, ToolCallPart
5
5
  from kosong.tooling import ToolResult
6
6
 
7
7
  from kimi_cli.soul import StatusSnapshot
8
- from kimi_cli.soul.wire import (
8
+ from kimi_cli.ui.shell.console import console
9
+ from kimi_cli.ui.shell.keyboard import listen_for_keyboard
10
+ from kimi_cli.ui.shell.liveview import StepLiveView, StepLiveViewWithMarkdown
11
+ from kimi_cli.utils.logging import logger
12
+ from kimi_cli.wire import WireUISide
13
+ from kimi_cli.wire.message import (
9
14
  ApprovalRequest,
10
15
  CompactionBegin,
11
16
  CompactionEnd,
12
17
  StatusUpdate,
13
18
  StepBegin,
14
19
  StepInterrupted,
15
- Wire,
16
20
  )
17
- from kimi_cli.ui.shell.console import console
18
- from kimi_cli.ui.shell.keyboard import listen_for_keyboard
19
- from kimi_cli.ui.shell.liveview import StepLiveView, StepLiveViewWithMarkdown
20
- from kimi_cli.utils.logging import logger
21
21
 
22
22
 
23
23
  @asynccontextmanager
@@ -25,7 +25,6 @@ async def _keyboard_listener(step: StepLiveView):
25
25
  async def _keyboard():
26
26
  try:
27
27
  async for event in listen_for_keyboard():
28
- # TODO: ESCAPE to interrupt
29
28
  step.handle_keyboard_event(event)
30
29
  except asyncio.CancelledError:
31
30
  return
@@ -39,10 +38,20 @@ async def _keyboard_listener(step: StepLiveView):
39
38
  await task
40
39
 
41
40
 
42
- async def visualize(wire: Wire, *, initial_status: StatusSnapshot):
41
+ async def visualize(
42
+ wire: WireUISide,
43
+ *,
44
+ initial_status: StatusSnapshot,
45
+ cancel_event: asyncio.Event | None = None,
46
+ ):
43
47
  """
44
48
  A loop to consume agent events and visualize the agent behavior.
45
49
  This loop never raise any exception except asyncio.CancelledError.
50
+
51
+ Args:
52
+ wire: Communication channel with the agent
53
+ initial_status: Initial status snapshot
54
+ cancel_event: Event that can be set (e.g., by ESC key) to cancel the run
46
55
  """
47
56
  latest_status = initial_status
48
57
  try:
@@ -52,7 +61,7 @@ async def visualize(wire: Wire, *, initial_status: StatusSnapshot):
52
61
  while True:
53
62
  # TODO: Maybe we can always have a StepLiveView here.
54
63
  # No need to recreate for each step.
55
- with StepLiveViewWithMarkdown(latest_status) as step:
64
+ with StepLiveViewWithMarkdown(latest_status, cancel_event) as step:
56
65
  async with _keyboard_listener(step):
57
66
  # spin the moon at the beginning of each step
58
67
  with console.status("", spinner="moon"):
@@ -0,0 +1,10 @@
1
+ import ssl
2
+
3
+ import aiohttp
4
+ import certifi
5
+
6
+ _ssl_context = ssl.create_default_context(cafile=certifi.where())
7
+
8
+
9
+ def new_client_session() -> aiohttp.ClientSession:
10
+ return aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=_ssl_context))
@@ -98,4 +98,6 @@ def format_release_notes(changelog: dict[str, ReleaseEntry]) -> str:
98
98
  return "\n".join(parts).strip()
99
99
 
100
100
 
101
- CHANGELOG = parse_changelog((Path(__file__).parent.parent / "CHANGELOG.md").read_text())
101
+ CHANGELOG = parse_changelog(
102
+ (Path(__file__).parent.parent / "CHANGELOG.md").read_text(encoding="utf-8")
103
+ )
@@ -0,0 +1,57 @@
1
+ import asyncio
2
+
3
+ from kosong.base.message import ContentPart, ToolCallPart
4
+
5
+ from kimi_cli.utils.logging import logger
6
+ from kimi_cli.wire.message import WireMessage
7
+
8
+
9
+ class Wire:
10
+ """
11
+ A channel for communication between the soul and the UI during a soul run.
12
+ """
13
+
14
+ def __init__(self):
15
+ self._queue = asyncio.Queue[WireMessage]()
16
+ self._soul_side = WireSoulSide(self._queue)
17
+ self._ui_side = WireUISide(self._queue)
18
+
19
+ @property
20
+ def soul_side(self) -> "WireSoulSide":
21
+ return self._soul_side
22
+
23
+ @property
24
+ def ui_side(self) -> "WireUISide":
25
+ return self._ui_side
26
+
27
+ def shutdown(self) -> None:
28
+ self._queue.shutdown()
29
+
30
+
31
+ class WireSoulSide:
32
+ """
33
+ The soul side of a wire.
34
+ """
35
+
36
+ def __init__(self, queue: asyncio.Queue[WireMessage]):
37
+ self._queue = queue
38
+
39
+ def send(self, msg: WireMessage) -> None:
40
+ if not isinstance(msg, ContentPart | ToolCallPart):
41
+ logger.debug("Sending wire message: {msg}", msg=msg)
42
+ self._queue.put_nowait(msg)
43
+
44
+
45
+ class WireUISide:
46
+ """
47
+ The UI side of a wire.
48
+ """
49
+
50
+ def __init__(self, queue: asyncio.Queue[WireMessage]):
51
+ self._queue = queue
52
+
53
+ async def receive(self) -> WireMessage:
54
+ msg = await self._queue.get()
55
+ if not isinstance(msg, ContentPart | ToolCallPart):
56
+ logger.debug("Receiving wire message: {msg}", msg=msg)
57
+ return msg
@@ -1,14 +1,13 @@
1
1
  import asyncio
2
2
  import uuid
3
- from contextvars import ContextVar
4
3
  from enum import Enum
5
- from typing import NamedTuple
4
+ from typing import TYPE_CHECKING, NamedTuple
6
5
 
7
6
  from kosong.base.message import ContentPart, ToolCall, ToolCallPart
8
7
  from kosong.tooling import ToolResult
9
8
 
10
- from kimi_cli.soul import StatusSnapshot
11
- from kimi_cli.utils.logging import logger
9
+ if TYPE_CHECKING:
10
+ from kimi_cli.soul import StatusSnapshot
12
11
 
13
12
 
14
13
  class StepBegin(NamedTuple):
@@ -39,7 +38,7 @@ class CompactionEnd:
39
38
 
40
39
 
41
40
  class StatusUpdate(NamedTuple):
42
- status: StatusSnapshot
41
+ status: "StatusSnapshot"
43
42
 
44
43
 
45
44
  type ControlFlowEvent = StepBegin | StepInterrupted | CompactionBegin | CompactionEnd | StatusUpdate
@@ -90,37 +89,3 @@ class ApprovalRequest:
90
89
 
91
90
 
92
91
  type WireMessage = Event | ApprovalRequest
93
-
94
-
95
- class Wire:
96
- """
97
- A channel for communication between the soul and the UI.
98
- """
99
-
100
- def __init__(self):
101
- self._queue = asyncio.Queue[WireMessage]()
102
-
103
- def send(self, msg: WireMessage) -> None:
104
- if not isinstance(msg, ContentPart | ToolCallPart):
105
- logger.debug("Sending wire message: {msg}", msg=msg)
106
- self._queue.put_nowait(msg)
107
-
108
- async def receive(self) -> WireMessage:
109
- msg = await self._queue.get()
110
- if not isinstance(msg, ContentPart | ToolCallPart):
111
- logger.debug("Receiving wire message: {msg}", msg=msg)
112
- return msg
113
-
114
- def shutdown(self) -> None:
115
- self._queue.shutdown()
116
-
117
-
118
- current_wire = ContextVar[Wire | None]("current_wire", default=None)
119
-
120
-
121
- def get_wire_or_none() -> Wire | None:
122
- """
123
- Get the current wire or None.
124
- Expect to be not None when called from anywhere in the agent loop.
125
- """
126
- return current_wire.get()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kimi-cli
3
- Version: 0.39
3
+ Version: 0.41
4
4
  Summary: Kimi CLI is your next CLI agent.
5
5
  Requires-Dist: agent-client-protocol>=0.4.9
6
6
  Requires-Dist: aiofiles>=25.1.0
@@ -24,6 +24,11 @@ Description-Content-Type: text/markdown
24
24
 
25
25
  # Kimi CLI
26
26
 
27
+ [![Commit Activity](https://img.shields.io/github/commit-activity/w/MoonshotAI/kimi-cli)](https://github.com/MoonshotAI/kimi-cli/graphs/commit-activity)
28
+ [![Checks](https://img.shields.io/github/check-runs/MoonshotAI/kimi-cli/main)](https://github.com/MoonshotAI/kimi-cli/actions)
29
+ [![Version](https://img.shields.io/pypi/v/kimi-cli)](https://pypi.org/project/kimi-cli/)
30
+ [![Downloads](https://img.shields.io/pypi/dw/kimi-cli)](https://pypistats.org/packages/kimi-cli)
31
+
27
32
  [中文](https://www.kimi.com/coding/docs/kimi-cli.html)
28
33
 
29
34
  Kimi CLI is a new CLI agent that can help you with your software development tasks and terminal operations.
@@ -108,7 +113,7 @@ After restarting Zsh, you can switch to agent mode by pressing `Ctrl-K`.
108
113
 
109
114
  ### ACP support
110
115
 
111
- Kimi CLI supports [Agent Client Protocol] out of the box. You can use it together with any ACP-compatible editors or IDEs.
116
+ Kimi CLI supports [Agent Client Protocol] out of the box. You can use it together with any ACP-compatible editor or IDE.
112
117
 
113
118
  For example, to use Kimi CLI with [Zed](https://zed.dev/), add the following configuration to your `~/.config/zed/settings.json`:
114
119
 
@@ -152,3 +157,31 @@ Run `kimi` with `--mcp-config-file` option to connect to the specified MCP serve
152
157
  ```sh
153
158
  kimi --mcp-config-file /path/to/mcp.json
154
159
  ```
160
+
161
+ ## Development
162
+
163
+ To develop Kimi CLI, run:
164
+
165
+ ```sh
166
+ git clone https://github.com/MoonshotAI/kimi-cli.git
167
+ cd kimi-cli
168
+
169
+ make prepare # prepare the development environment
170
+ ```
171
+
172
+ Then you can start working on Kimi CLI.
173
+
174
+ Refer to the following commands after you make changes:
175
+
176
+ ```sh
177
+ uv run kimi # run Kimi CLI
178
+
179
+ make format # format code
180
+ make check # run linting and type checking
181
+ make test # run tests
182
+ make help # show all make targets
183
+ ```
184
+
185
+ ## Contributing
186
+
187
+ We welcome contributions to Kimi CLI! Please refer to [CONTRIBUTING.md](./CONTRIBUTING.md) for more information.