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.
- kimi_cli/CHANGELOG.md +23 -0
- kimi_cli/__init__.py +18 -280
- kimi_cli/agents/koder/system.md +1 -1
- kimi_cli/agentspec.py +104 -0
- kimi_cli/cli.py +235 -0
- kimi_cli/constant.py +4 -0
- kimi_cli/llm.py +69 -0
- kimi_cli/prompts/__init__.py +2 -2
- kimi_cli/soul/__init__.py +102 -6
- kimi_cli/soul/agent.py +157 -0
- kimi_cli/soul/approval.py +2 -4
- kimi_cli/soul/compaction.py +10 -10
- kimi_cli/soul/context.py +5 -0
- kimi_cli/soul/globals.py +92 -0
- kimi_cli/soul/kimisoul.py +26 -30
- kimi_cli/soul/message.py +5 -5
- kimi_cli/tools/bash/__init__.py +2 -2
- kimi_cli/tools/dmail/__init__.py +1 -1
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +2 -2
- kimi_cli/tools/file/grep.py +3 -2
- kimi_cli/tools/file/patch.py +2 -2
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +2 -2
- kimi_cli/tools/file/write.py +2 -2
- kimi_cli/tools/task/__init__.py +23 -22
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +1 -1
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +4 -4
- kimi_cli/ui/__init__.py +0 -69
- kimi_cli/ui/acp/__init__.py +8 -9
- kimi_cli/ui/print/__init__.py +17 -35
- kimi_cli/ui/shell/__init__.py +9 -15
- kimi_cli/ui/shell/liveview.py +13 -4
- kimi_cli/ui/shell/metacmd.py +3 -3
- kimi_cli/ui/shell/setup.py +7 -6
- kimi_cli/ui/shell/update.py +4 -3
- kimi_cli/ui/shell/visualize.py +18 -9
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +3 -1
- kimi_cli/wire/__init__.py +57 -0
- kimi_cli/{soul/wire.py → wire/message.py} +4 -39
- {kimi_cli-0.39.dist-info → kimi_cli-0.41.dist-info}/METADATA +35 -2
- kimi_cli-0.41.dist-info/RECORD +85 -0
- kimi_cli-0.41.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/utils/provider.py +0 -72
- kimi_cli-0.39.dist-info/RECORD +0 -80
- kimi_cli-0.39.dist-info/entry_points.txt +0 -3
- {kimi_cli-0.39.dist-info → kimi_cli-0.41.dist-info}/WHEEL +0 -0
kimi_cli/ui/acp/__init__.py
CHANGED
|
@@ -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.
|
|
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
|
|
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:
|
|
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:
|
|
430
|
+
lambda conn: ACPAgent(self.soul, conn),
|
|
432
431
|
writer,
|
|
433
432
|
reader,
|
|
434
433
|
)
|
kimi_cli/ui/print/__init__.py
CHANGED
|
@@ -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 (
|
|
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__(
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
kimi_cli/ui/shell/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
kimi_cli/ui/shell/liveview.py
CHANGED
|
@@ -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
|
kimi_cli/ui/shell/metacmd.py
CHANGED
|
@@ -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
|
|
176
|
+
from kimi_cli.constant import VERSION
|
|
177
177
|
|
|
178
|
-
console.print(f"kimi, version {
|
|
178
|
+
console.print(f"kimi, version {VERSION}")
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
@meta_command(name="release-notes")
|
kimi_cli/ui/shell/setup.py
CHANGED
|
@@ -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
|
-
|
|
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
|
kimi_cli/ui/shell/update.py
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
kimi_cli/ui/shell/visualize.py
CHANGED
|
@@ -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.
|
|
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(
|
|
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"):
|
kimi_cli/utils/changelog.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
11
|
-
from kimi_cli.
|
|
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.
|
|
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
|
+
[](https://github.com/MoonshotAI/kimi-cli/graphs/commit-activity)
|
|
28
|
+
[](https://github.com/MoonshotAI/kimi-cli/actions)
|
|
29
|
+
[](https://pypi.org/project/kimi-cli/)
|
|
30
|
+
[](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
|
|
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.
|