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.
- kimi_cli/CHANGELOG.md +28 -0
- kimi_cli/__init__.py +169 -102
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agentspec.py +19 -8
- kimi_cli/cli.py +51 -37
- kimi_cli/config.py +33 -14
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +31 -3
- kimi_cli/metadata.py +5 -68
- kimi_cli/session.py +81 -0
- kimi_cli/soul/__init__.py +22 -4
- kimi_cli/soul/agent.py +18 -23
- kimi_cli/soul/context.py +0 -5
- kimi_cli/soul/kimisoul.py +40 -25
- kimi_cli/soul/message.py +1 -1
- kimi_cli/soul/{globals.py → runtime.py} +13 -11
- kimi_cli/tools/file/glob.py +1 -1
- kimi_cli/tools/file/patch.py +1 -1
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +1 -1
- kimi_cli/tools/file/write.py +1 -1
- kimi_cli/tools/task/__init__.py +29 -21
- kimi_cli/tools/web/search.py +3 -0
- kimi_cli/ui/acp/__init__.py +24 -28
- kimi_cli/ui/print/__init__.py +27 -30
- kimi_cli/ui/shell/__init__.py +58 -42
- kimi_cli/ui/shell/keyboard.py +82 -14
- kimi_cli/ui/shell/metacmd.py +3 -8
- kimi_cli/ui/shell/prompt.py +208 -6
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/visualize.py +54 -57
- kimi_cli/utils/message.py +14 -0
- kimi_cli/utils/signals.py +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/wire/__init__.py +13 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/METADATA +21 -20
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/RECORD +41 -38
- kimi_cli/agents/koder/README.md +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/agents/{koder → default}/system.md +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/WHEEL +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.43.dist-info}/entry_points.txt +0 -0
kimi_cli/ui/shell/__init__.py
CHANGED
|
@@ -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:
|
|
27
|
+
def __init__(self, soul: Soul, welcome_info: list["WelcomeInfoItem"] | None = None):
|
|
24
28
|
self.soul = soul
|
|
25
|
-
self.
|
|
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.
|
|
38
|
+
self._start_background_task(self._auto_update())
|
|
35
39
|
|
|
36
|
-
_print_welcome_info(self.soul.name or "Kimi CLI", self.
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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=
|
|
72
|
-
await self._run_soul_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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
remove_sigint()
|
|
192
208
|
return False
|
|
193
209
|
|
|
194
|
-
def
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
kimi_cli/ui/shell/keyboard.py
CHANGED
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
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
|
|
kimi_cli/ui/shell/metacmd.py
CHANGED
|
@@ -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.
|
|
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. "
|