kimi-cli 0.35__py3-none-any.whl → 0.52__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.
- kimi_cli/CHANGELOG.md +165 -0
- kimi_cli/__init__.py +0 -374
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/app.py +208 -0
- kimi_cli/cli.py +321 -0
- kimi_cli/config.py +33 -16
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +144 -3
- kimi_cli/metadata.py +6 -69
- kimi_cli/prompts/__init__.py +4 -0
- kimi_cli/session.py +103 -0
- kimi_cli/soul/__init__.py +130 -9
- kimi_cli/soul/agent.py +159 -0
- kimi_cli/soul/approval.py +5 -6
- kimi_cli/soul/compaction.py +106 -0
- kimi_cli/soul/context.py +1 -1
- kimi_cli/soul/kimisoul.py +180 -80
- kimi_cli/soul/message.py +6 -6
- kimi_cli/soul/runtime.py +96 -0
- kimi_cli/soul/toolset.py +3 -2
- kimi_cli/tools/__init__.py +35 -31
- kimi_cli/tools/bash/__init__.py +25 -9
- kimi_cli/tools/bash/cmd.md +31 -0
- kimi_cli/tools/dmail/__init__.py +5 -4
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +4 -4
- kimi_cli/tools/file/grep.py +36 -19
- kimi_cli/tools/file/patch.py +52 -10
- kimi_cli/tools/file/read.py +6 -5
- kimi_cli/tools/file/replace.py +16 -4
- kimi_cli/tools/file/write.py +16 -4
- kimi_cli/tools/mcp.py +7 -4
- kimi_cli/tools/task/__init__.py +60 -41
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +4 -2
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +13 -12
- kimi_cli/ui/__init__.py +0 -68
- kimi_cli/ui/acp/__init__.py +67 -38
- kimi_cli/ui/print/__init__.py +46 -69
- kimi_cli/ui/shell/__init__.py +145 -154
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +183 -0
- kimi_cli/ui/shell/metacmd.py +34 -81
- kimi_cli/ui/shell/prompt.py +245 -28
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/setup.py +19 -19
- kimi_cli/ui/shell/update.py +11 -5
- kimi_cli/ui/shell/visualize.py +576 -0
- kimi_cli/ui/wire/README.md +109 -0
- kimi_cli/ui/wire/__init__.py +340 -0
- kimi_cli/ui/wire/jsonrpc.py +48 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +6 -2
- kimi_cli/utils/clipboard.py +10 -0
- kimi_cli/utils/message.py +15 -1
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/markdown.py +959 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/utils/term.py +114 -0
- kimi_cli/wire/__init__.py +73 -0
- kimi_cli/wire/message.py +191 -0
- kimi_cli-0.52.dist-info/METADATA +186 -0
- kimi_cli-0.52.dist-info/RECORD +99 -0
- kimi_cli-0.52.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/prompts/metacmds/__init__.py +0 -4
- kimi_cli/soul/wire.py +0 -101
- kimi_cli/ui/shell/liveview.py +0 -158
- kimi_cli/utils/provider.py +0 -64
- kimi_cli-0.35.dist-info/METADATA +0 -24
- kimi_cli-0.35.dist-info/RECORD +0 -76
- kimi_cli-0.35.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
- /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import acp # pyright: ignore[reportMissingTypeStubs]
|
|
8
|
+
from kosong.chat_provider import ChatProviderError
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from kimi_cli.soul import LLMNotSet, LLMNotSupported, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
12
|
+
from kimi_cli.utils.logging import logger
|
|
13
|
+
from kimi_cli.wire import WireUISide
|
|
14
|
+
from kimi_cli.wire.message import (
|
|
15
|
+
ApprovalRequest,
|
|
16
|
+
ApprovalResponse,
|
|
17
|
+
Event,
|
|
18
|
+
serialize_approval_request,
|
|
19
|
+
serialize_event,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .jsonrpc import (
|
|
23
|
+
JSONRPC_MESSAGE_ADAPTER,
|
|
24
|
+
JSONRPC_VERSION,
|
|
25
|
+
JSONRPCErrorResponse,
|
|
26
|
+
JSONRPCRequest,
|
|
27
|
+
JSONRPCSuccessResponse,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_ResultKind = Literal["ok", "error"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _SoulRunner:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
soul: Soul,
|
|
37
|
+
send_event: Callable[[Event], Awaitable[None]],
|
|
38
|
+
request_approval: Callable[[ApprovalRequest], Awaitable[ApprovalResponse]],
|
|
39
|
+
):
|
|
40
|
+
self._soul = soul
|
|
41
|
+
self._send_event = send_event
|
|
42
|
+
self._request_approval = request_approval
|
|
43
|
+
self._cancel_event: asyncio.Event | None = None
|
|
44
|
+
self._task: asyncio.Task[tuple[_ResultKind, Any]] | None = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def is_running(self) -> bool:
|
|
48
|
+
return self._task is not None and not self._task.done()
|
|
49
|
+
|
|
50
|
+
async def run(self, user_input: str) -> tuple[_ResultKind, Any]:
|
|
51
|
+
if self.is_running:
|
|
52
|
+
raise RuntimeError("Soul is already running")
|
|
53
|
+
|
|
54
|
+
self._cancel_event = asyncio.Event()
|
|
55
|
+
self._task = asyncio.create_task(self._run(user_input))
|
|
56
|
+
try:
|
|
57
|
+
return await self._task
|
|
58
|
+
finally:
|
|
59
|
+
self._task = None
|
|
60
|
+
self._cancel_event = None
|
|
61
|
+
|
|
62
|
+
async def interrupt(self) -> None:
|
|
63
|
+
if self._cancel_event is not None:
|
|
64
|
+
self._cancel_event.set()
|
|
65
|
+
|
|
66
|
+
async def shutdown(self) -> None:
|
|
67
|
+
await self.interrupt()
|
|
68
|
+
if self._task is not None:
|
|
69
|
+
self._task.cancel()
|
|
70
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
71
|
+
await self._task
|
|
72
|
+
self._task = None
|
|
73
|
+
self._cancel_event = None
|
|
74
|
+
|
|
75
|
+
async def _run(self, user_input: str) -> tuple[_ResultKind, Any]:
|
|
76
|
+
assert self._cancel_event is not None
|
|
77
|
+
try:
|
|
78
|
+
await run_soul(
|
|
79
|
+
self._soul,
|
|
80
|
+
user_input,
|
|
81
|
+
self._ui_loop,
|
|
82
|
+
self._cancel_event,
|
|
83
|
+
)
|
|
84
|
+
except LLMNotSet:
|
|
85
|
+
return ("error", (-32001, "LLM is not configured"))
|
|
86
|
+
except LLMNotSupported as e:
|
|
87
|
+
return ("error", (-32003, f"LLM not supported: {e}"))
|
|
88
|
+
except ChatProviderError as e:
|
|
89
|
+
return ("error", (-32002, f"LLM provider error: {e}"))
|
|
90
|
+
except MaxStepsReached as e:
|
|
91
|
+
return ("ok", {"status": "max_steps_reached", "steps": e.n_steps})
|
|
92
|
+
except RunCancelled:
|
|
93
|
+
return ("ok", {"status": "cancelled"})
|
|
94
|
+
except asyncio.CancelledError:
|
|
95
|
+
raise
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.exception("Soul run failed:")
|
|
98
|
+
return ("error", (-32099, f"Run failed: {e}"))
|
|
99
|
+
return ("ok", {"status": "finished"})
|
|
100
|
+
|
|
101
|
+
async def _ui_loop(self, wire: WireUISide) -> None:
|
|
102
|
+
while True:
|
|
103
|
+
message = await wire.receive()
|
|
104
|
+
if isinstance(message, ApprovalRequest):
|
|
105
|
+
response = await self._request_approval(message)
|
|
106
|
+
message.resolve(response)
|
|
107
|
+
else:
|
|
108
|
+
# must be Event
|
|
109
|
+
await self._send_event(message)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class WireServer:
|
|
113
|
+
def __init__(self, soul: Soul):
|
|
114
|
+
self._reader: asyncio.StreamReader | None = None
|
|
115
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
116
|
+
self._write_task: asyncio.Task[None] | None = None
|
|
117
|
+
self._send_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
|
118
|
+
self._pending_requests: dict[str, ApprovalRequest] = {}
|
|
119
|
+
self._runner = _SoulRunner(
|
|
120
|
+
soul,
|
|
121
|
+
send_event=self._send_event,
|
|
122
|
+
request_approval=self._request_approval,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def run(self) -> bool:
|
|
126
|
+
logger.info("Starting Wire server on stdio")
|
|
127
|
+
|
|
128
|
+
self._reader, self._writer = await acp.stdio_streams()
|
|
129
|
+
self._write_task = asyncio.create_task(self._write_loop())
|
|
130
|
+
try:
|
|
131
|
+
await self._read_loop()
|
|
132
|
+
finally:
|
|
133
|
+
await self._shutdown()
|
|
134
|
+
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
async def _read_loop(self) -> None:
|
|
138
|
+
assert self._reader is not None
|
|
139
|
+
|
|
140
|
+
while True:
|
|
141
|
+
line = await self._reader.readline()
|
|
142
|
+
if not line:
|
|
143
|
+
logger.info("stdin closed, Wire server exiting")
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
payload = json.loads(line.decode("utf-8"))
|
|
148
|
+
except json.JSONDecodeError:
|
|
149
|
+
logger.warning("Invalid JSON line: {line}", line=line)
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
await self._dispatch(payload)
|
|
153
|
+
|
|
154
|
+
async def _dispatch(self, payload: dict[str, Any]) -> None:
|
|
155
|
+
version = payload.get("jsonrpc")
|
|
156
|
+
if version != JSONRPC_VERSION:
|
|
157
|
+
logger.warning("Unexpected jsonrpc version: {version}", version=version)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
message = JSONRPC_MESSAGE_ADAPTER.validate_python(payload)
|
|
162
|
+
except ValidationError as e:
|
|
163
|
+
logger.warning(
|
|
164
|
+
"Ignoring malformed JSON-RPC payload: {message}; error={error}",
|
|
165
|
+
message=payload,
|
|
166
|
+
error=str(e),
|
|
167
|
+
)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
match message:
|
|
171
|
+
case JSONRPCRequest():
|
|
172
|
+
await self._handle_request(message)
|
|
173
|
+
case JSONRPCSuccessResponse() | JSONRPCErrorResponse():
|
|
174
|
+
await self._handle_response(message)
|
|
175
|
+
|
|
176
|
+
async def _handle_request(self, message: JSONRPCRequest) -> None:
|
|
177
|
+
method = message.method
|
|
178
|
+
msg_id = message.id
|
|
179
|
+
params = message.params
|
|
180
|
+
|
|
181
|
+
if method == "run":
|
|
182
|
+
await self._handle_run(msg_id, params)
|
|
183
|
+
elif method == "interrupt":
|
|
184
|
+
await self._handle_interrupt(msg_id)
|
|
185
|
+
else:
|
|
186
|
+
logger.warning("Unknown method: {method}", method=method)
|
|
187
|
+
if msg_id is not None:
|
|
188
|
+
await self._send_error(msg_id, -32601, f"Unknown method: {method}")
|
|
189
|
+
|
|
190
|
+
async def _handle_response(
|
|
191
|
+
self,
|
|
192
|
+
message: JSONRPCSuccessResponse | JSONRPCErrorResponse,
|
|
193
|
+
) -> None:
|
|
194
|
+
msg_id = message.id
|
|
195
|
+
if msg_id is None:
|
|
196
|
+
logger.warning("Response without id: {message}", message=message.model_dump())
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
pending = self._pending_requests.get(msg_id)
|
|
200
|
+
if pending is None:
|
|
201
|
+
logger.warning("No pending request for response id={id}", id=msg_id)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
if isinstance(message, JSONRPCErrorResponse):
|
|
206
|
+
pending.resolve(ApprovalResponse.REJECT)
|
|
207
|
+
else:
|
|
208
|
+
response = self._parse_approval_response(message.result)
|
|
209
|
+
pending.resolve(response)
|
|
210
|
+
finally:
|
|
211
|
+
self._pending_requests.pop(msg_id, None)
|
|
212
|
+
|
|
213
|
+
async def _handle_run(self, msg_id: Any, params: dict[str, Any]) -> None:
|
|
214
|
+
if msg_id is None:
|
|
215
|
+
logger.warning("Run notification ignored")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if self._runner.is_running:
|
|
219
|
+
await self._send_error(msg_id, -32000, "Run already in progress")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
user_input = params.get("input")
|
|
223
|
+
if not isinstance(user_input, str):
|
|
224
|
+
user_input = params.get("prompt")
|
|
225
|
+
if not isinstance(user_input, str):
|
|
226
|
+
await self._send_error(msg_id, -32602, "`input` (or `prompt`) must be a string")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
kind, payload = await self._runner.run(user_input)
|
|
231
|
+
except RuntimeError:
|
|
232
|
+
await self._send_error(msg_id, -32000, "Run already in progress")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
if kind == "error":
|
|
236
|
+
code, message = payload
|
|
237
|
+
await self._send_error(msg_id, code, message)
|
|
238
|
+
else:
|
|
239
|
+
await self._send_response(msg_id, payload)
|
|
240
|
+
|
|
241
|
+
async def _handle_interrupt(self, msg_id: Any) -> None:
|
|
242
|
+
if not self._runner.is_running:
|
|
243
|
+
if msg_id is not None:
|
|
244
|
+
await self._send_response(msg_id, {"status": "idle"})
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
await self._runner.interrupt()
|
|
248
|
+
if msg_id is not None:
|
|
249
|
+
await self._send_response(msg_id, {"status": "ok"})
|
|
250
|
+
|
|
251
|
+
async def _send_event(self, event: Event) -> None:
|
|
252
|
+
await self._send_notification("event", serialize_event(event))
|
|
253
|
+
|
|
254
|
+
async def _request_approval(self, request: ApprovalRequest) -> ApprovalResponse:
|
|
255
|
+
self._pending_requests[request.id] = request
|
|
256
|
+
|
|
257
|
+
await self._send_request(
|
|
258
|
+
request.id,
|
|
259
|
+
"request",
|
|
260
|
+
{"type": "approval", "payload": serialize_approval_request(request)},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
return await request.wait()
|
|
265
|
+
finally:
|
|
266
|
+
self._pending_requests.pop(request.id, None)
|
|
267
|
+
|
|
268
|
+
def _parse_approval_response(self, result: dict[str, Any]) -> ApprovalResponse:
|
|
269
|
+
value = result.get("response")
|
|
270
|
+
try:
|
|
271
|
+
if isinstance(value, ApprovalResponse):
|
|
272
|
+
return value
|
|
273
|
+
return ApprovalResponse(str(value))
|
|
274
|
+
except ValueError:
|
|
275
|
+
logger.warning("Unknown approval response: {value}", value=value)
|
|
276
|
+
return ApprovalResponse.REJECT
|
|
277
|
+
|
|
278
|
+
async def _write_loop(self) -> None:
|
|
279
|
+
assert self._writer is not None
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
while True:
|
|
283
|
+
try:
|
|
284
|
+
payload = await self._send_queue.get()
|
|
285
|
+
except asyncio.QueueShutDown:
|
|
286
|
+
logger.debug("Send queue shut down, stopping Wire server write loop")
|
|
287
|
+
break
|
|
288
|
+
data = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
|
289
|
+
self._writer.write(data.encode("utf-8") + b"\n")
|
|
290
|
+
await self._writer.drain()
|
|
291
|
+
except asyncio.CancelledError:
|
|
292
|
+
raise
|
|
293
|
+
except Exception:
|
|
294
|
+
logger.exception("Wire server write loop error:")
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
async def _send_notification(self, method: str, params: Any) -> None:
|
|
298
|
+
await self._enqueue_payload(
|
|
299
|
+
{"jsonrpc": JSONRPC_VERSION, "method": method, "params": params}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
async def _send_request(self, msg_id: Any, method: str, params: Any) -> None:
|
|
303
|
+
await self._enqueue_payload(
|
|
304
|
+
{"jsonrpc": JSONRPC_VERSION, "id": msg_id, "method": method, "params": params}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
async def _send_response(self, msg_id: Any, result: Any) -> None:
|
|
308
|
+
await self._enqueue_payload({"jsonrpc": JSONRPC_VERSION, "id": msg_id, "result": result})
|
|
309
|
+
|
|
310
|
+
async def _send_error(self, msg_id: Any, code: int, message: str) -> None:
|
|
311
|
+
await self._enqueue_payload(
|
|
312
|
+
{"jsonrpc": JSONRPC_VERSION, "id": msg_id, "error": {"code": code, "message": message}}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def _enqueue_payload(self, payload: dict[str, Any]) -> None:
|
|
316
|
+
try:
|
|
317
|
+
await self._send_queue.put(payload)
|
|
318
|
+
except asyncio.QueueShutDown:
|
|
319
|
+
logger.debug("Send queue shut down; dropping payload: {payload}", payload=payload)
|
|
320
|
+
|
|
321
|
+
async def _shutdown(self) -> None:
|
|
322
|
+
await self._runner.shutdown()
|
|
323
|
+
self._send_queue.shutdown()
|
|
324
|
+
|
|
325
|
+
if self._write_task is not None:
|
|
326
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
327
|
+
await self._write_task
|
|
328
|
+
|
|
329
|
+
for request in self._pending_requests.values():
|
|
330
|
+
if not request.resolved:
|
|
331
|
+
request.resolve(ApprovalResponse.REJECT)
|
|
332
|
+
self._pending_requests.clear()
|
|
333
|
+
|
|
334
|
+
if self._writer is not None:
|
|
335
|
+
self._writer.close()
|
|
336
|
+
with contextlib.suppress(Exception):
|
|
337
|
+
await self._writer.wait_closed()
|
|
338
|
+
self._writer = None
|
|
339
|
+
|
|
340
|
+
self._reader = None
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
|
4
|
+
|
|
5
|
+
JSONRPC_VERSION = "2.0"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _MessageBase(BaseModel):
|
|
9
|
+
jsonrpc: Literal["2.0"]
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(extra="forbid")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JSONRPCRequest(_MessageBase):
|
|
15
|
+
method: str
|
|
16
|
+
id: str | None = None
|
|
17
|
+
params: dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _ResponseBase(_MessageBase):
|
|
21
|
+
id: str | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JSONRPCSuccessResponse(_ResponseBase):
|
|
25
|
+
result: dict[str, Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JSONRPCErrorObject(BaseModel):
|
|
29
|
+
code: int
|
|
30
|
+
message: str
|
|
31
|
+
data: Any | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JSONRPCErrorResponse(_ResponseBase):
|
|
35
|
+
error: JSONRPCErrorObject
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
JSONRPCMessage = JSONRPCRequest | JSONRPCSuccessResponse | JSONRPCErrorResponse
|
|
39
|
+
JSONRPC_MESSAGE_ADAPTER = TypeAdapter[JSONRPCMessage](JSONRPCMessage)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"JSONRPCRequest",
|
|
43
|
+
"JSONRPCSuccessResponse",
|
|
44
|
+
"JSONRPCErrorObject",
|
|
45
|
+
"JSONRPCErrorResponse",
|
|
46
|
+
"JSONRPCMessage",
|
|
47
|
+
"JSONRPC_MESSAGE_ADAPTER",
|
|
48
|
+
]
|
|
File without changes
|
kimi_cli/utils/changelog.py
CHANGED
|
@@ -85,7 +85,7 @@ def parse_changelog(md_text: str) -> dict[str, ReleaseEntry]:
|
|
|
85
85
|
return result
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def format_release_notes(changelog: dict[str, ReleaseEntry]) -> str:
|
|
88
|
+
def format_release_notes(changelog: dict[str, ReleaseEntry], include_lib_changes: bool) -> str:
|
|
89
89
|
parts: list[str] = []
|
|
90
90
|
for ver, entry in changelog.items():
|
|
91
91
|
s = f"[bold]{ver}[/bold]"
|
|
@@ -93,9 +93,13 @@ def format_release_notes(changelog: dict[str, ReleaseEntry]) -> str:
|
|
|
93
93
|
s += f": {entry.description}"
|
|
94
94
|
if entry.entries:
|
|
95
95
|
for it in entry.entries:
|
|
96
|
+
if it.lower().startswith("lib:") and not include_lib_changes:
|
|
97
|
+
continue
|
|
96
98
|
s += "\n[markdown.item.bullet]• [/]" + it
|
|
97
99
|
parts.append(s + "\n")
|
|
98
100
|
return "\n".join(parts).strip()
|
|
99
101
|
|
|
100
102
|
|
|
101
|
-
CHANGELOG = parse_changelog(
|
|
103
|
+
CHANGELOG = parse_changelog(
|
|
104
|
+
(Path(__file__).parent.parent / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
105
|
+
)
|
kimi_cli/utils/message.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from kosong.
|
|
1
|
+
from kosong.message import Message, TextPart
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def message_extract_text(message: Message) -> str:
|
|
@@ -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,33 @@
|
|
|
1
|
+
"""Project-wide Rich configuration helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
from rich import _wrap
|
|
9
|
+
|
|
10
|
+
# Regex used by Rich to compute break opportunities during wrapping.
|
|
11
|
+
_DEFAULT_WRAP_PATTERN: Final[re.Pattern[str]] = re.compile(r"\s*\S+\s*")
|
|
12
|
+
_CHAR_WRAP_PATTERN: Final[re.Pattern[str]] = re.compile(r".", re.DOTALL)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def enable_character_wrap() -> None:
|
|
16
|
+
"""Switch Rich's wrapping logic to break on every character.
|
|
17
|
+
|
|
18
|
+
Rich's default behavior tries to preserve whole words; we override the
|
|
19
|
+
internal regex so markdown rendering can fold text at any column once it
|
|
20
|
+
exceeds the terminal width.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_wrap.re_word = _CHAR_WRAP_PATTERN
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def restore_word_wrap() -> None:
|
|
27
|
+
"""Restore Rich's default word-based wrapping."""
|
|
28
|
+
|
|
29
|
+
_wrap.re_word = _DEFAULT_WRAP_PATTERN
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Apply character-based wrapping globally for the CLI.
|
|
33
|
+
enable_character_wrap()
|