kimi-cli 0.44__py3-none-any.whl → 0.78__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 +349 -40
- kimi_cli/__init__.py +6 -0
- kimi_cli/acp/AGENTS.md +91 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +111 -0
- kimi_cli/acp/kaos.py +270 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +335 -0
- kimi_cli/acp/session.py +445 -0
- kimi_cli/acp/tools.py +158 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/agents/default/agent.yaml +4 -4
- kimi_cli/agents/default/sub.yaml +2 -1
- kimi_cli/agents/default/system.md +79 -21
- kimi_cli/agents/okabe/agent.yaml +17 -0
- kimi_cli/agentspec.py +53 -25
- kimi_cli/app.py +180 -52
- kimi_cli/cli/__init__.py +595 -0
- kimi_cli/cli/__main__.py +8 -0
- kimi_cli/cli/info.py +63 -0
- kimi_cli/cli/mcp.py +349 -0
- kimi_cli/config.py +153 -17
- kimi_cli/constant.py +3 -0
- kimi_cli/exception.py +23 -2
- kimi_cli/flow/__init__.py +117 -0
- kimi_cli/flow/d2.py +376 -0
- kimi_cli/flow/mermaid.py +218 -0
- kimi_cli/llm.py +129 -23
- kimi_cli/metadata.py +32 -7
- kimi_cli/platforms.py +262 -0
- kimi_cli/prompts/__init__.py +2 -0
- kimi_cli/prompts/compact.md +4 -5
- kimi_cli/session.py +223 -31
- kimi_cli/share.py +2 -0
- kimi_cli/skill.py +145 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +351 -0
- kimi_cli/soul/__init__.py +51 -20
- kimi_cli/soul/agent.py +213 -85
- kimi_cli/soul/approval.py +86 -17
- kimi_cli/soul/compaction.py +64 -53
- kimi_cli/soul/context.py +38 -5
- kimi_cli/soul/denwarenji.py +2 -0
- kimi_cli/soul/kimisoul.py +442 -60
- kimi_cli/soul/message.py +54 -54
- kimi_cli/soul/slash.py +72 -0
- kimi_cli/soul/toolset.py +387 -6
- kimi_cli/toad.py +74 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +42 -34
- kimi_cli/tools/display.py +25 -0
- kimi_cli/tools/dmail/__init__.py +10 -10
- kimi_cli/tools/dmail/dmail.md +11 -9
- kimi_cli/tools/file/__init__.py +1 -3
- kimi_cli/tools/file/glob.py +20 -23
- kimi_cli/tools/file/grep.md +1 -1
- kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
- kimi_cli/tools/file/read.md +24 -6
- kimi_cli/tools/file/read.py +134 -50
- kimi_cli/tools/file/replace.md +1 -1
- kimi_cli/tools/file/replace.py +36 -29
- kimi_cli/tools/file/utils.py +282 -0
- kimi_cli/tools/file/write.py +43 -22
- kimi_cli/tools/multiagent/__init__.py +7 -0
- kimi_cli/tools/multiagent/create.md +11 -0
- kimi_cli/tools/multiagent/create.py +50 -0
- kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
- kimi_cli/tools/shell/__init__.py +120 -0
- kimi_cli/tools/{bash → shell}/bash.md +1 -2
- kimi_cli/tools/shell/powershell.md +25 -0
- kimi_cli/tools/test.py +4 -4
- kimi_cli/tools/think/__init__.py +2 -2
- kimi_cli/tools/todo/__init__.py +14 -8
- kimi_cli/tools/utils.py +64 -24
- kimi_cli/tools/web/fetch.py +68 -13
- kimi_cli/tools/web/search.py +10 -12
- kimi_cli/ui/acp/__init__.py +65 -412
- kimi_cli/ui/print/__init__.py +37 -49
- kimi_cli/ui/print/visualize.py +179 -0
- kimi_cli/ui/shell/__init__.py +141 -84
- kimi_cli/ui/shell/console.py +2 -0
- kimi_cli/ui/shell/debug.py +28 -23
- kimi_cli/ui/shell/keyboard.py +5 -1
- kimi_cli/ui/shell/prompt.py +220 -194
- kimi_cli/ui/shell/replay.py +111 -46
- kimi_cli/ui/shell/setup.py +89 -82
- kimi_cli/ui/shell/slash.py +422 -0
- kimi_cli/ui/shell/update.py +4 -2
- kimi_cli/ui/shell/usage.py +271 -0
- kimi_cli/ui/shell/visualize.py +574 -72
- kimi_cli/ui/wire/__init__.py +267 -0
- kimi_cli/ui/wire/jsonrpc.py +142 -0
- kimi_cli/ui/wire/protocol.py +1 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +2 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +12 -7
- kimi_cli/utils/clipboard.py +12 -0
- kimi_cli/utils/datetime.py +37 -0
- kimi_cli/utils/environment.py +58 -0
- kimi_cli/utils/envvar.py +12 -0
- kimi_cli/utils/frontmatter.py +44 -0
- kimi_cli/utils/logging.py +7 -6
- kimi_cli/utils/message.py +9 -14
- kimi_cli/utils/path.py +99 -9
- kimi_cli/utils/pyinstaller.py +6 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/markdown.py +961 -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 +2 -0
- kimi_cli/utils/slashcmd.py +124 -0
- kimi_cli/utils/string.py +2 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/wire/__init__.py +98 -29
- kimi_cli/wire/serde.py +45 -0
- kimi_cli/wire/types.py +299 -0
- kimi_cli-0.78.dist-info/METADATA +200 -0
- kimi_cli-0.78.dist-info/RECORD +135 -0
- kimi_cli-0.78.dist-info/entry_points.txt +4 -0
- kimi_cli/cli.py +0 -250
- kimi_cli/soul/runtime.py +0 -96
- kimi_cli/tools/bash/__init__.py +0 -99
- kimi_cli/tools/file/patch.md +0 -8
- kimi_cli/tools/file/patch.py +0 -143
- kimi_cli/tools/mcp.py +0 -85
- kimi_cli/ui/shell/liveview.py +0 -386
- kimi_cli/ui/shell/metacmd.py +0 -262
- kimi_cli/wire/message.py +0 -91
- kimi_cli-0.44.dist-info/METADATA +0 -188
- kimi_cli-0.44.dist-info/RECORD +0 -89
- kimi_cli-0.44.dist-info/entry_points.txt +0 -3
- /kimi_cli/tools/{task → multiagent}/task.md +0 -0
- {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
|
|
6
|
+
import acp # type: ignore[reportMissingTypeStubs]
|
|
7
|
+
import pydantic
|
|
8
|
+
from kosong.chat_provider import ChatProviderError
|
|
9
|
+
|
|
10
|
+
from kimi_cli.soul import LLMNotSet, LLMNotSupported, MaxStepsReached, RunCancelled, Soul, run_soul
|
|
11
|
+
from kimi_cli.soul.kimisoul import KimiSoul
|
|
12
|
+
from kimi_cli.utils.aioqueue import Queue, QueueShutDown
|
|
13
|
+
from kimi_cli.utils.logging import logger
|
|
14
|
+
from kimi_cli.wire import Wire
|
|
15
|
+
from kimi_cli.wire.types import ApprovalRequest, Request
|
|
16
|
+
|
|
17
|
+
from .jsonrpc import (
|
|
18
|
+
ErrorCodes,
|
|
19
|
+
JSONRPCApprovalRequestResult,
|
|
20
|
+
JSONRPCCancelMessage,
|
|
21
|
+
JSONRPCErrorObject,
|
|
22
|
+
JSONRPCErrorResponse,
|
|
23
|
+
JSONRPCEventMessage,
|
|
24
|
+
JSONRPCInMessage,
|
|
25
|
+
JSONRPCInMessageAdapter,
|
|
26
|
+
JSONRPCOutMessage,
|
|
27
|
+
JSONRPCPromptMessage,
|
|
28
|
+
JSONRPCRequestMessage,
|
|
29
|
+
JSONRPCSuccessResponse,
|
|
30
|
+
Statuses,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Maximum buffer size for the asyncio StreamReader used for stdio.
|
|
34
|
+
# Passed as the `limit` argument to `acp.stdio_streams`, this caps how much
|
|
35
|
+
# data can be buffered when reading from stdin (e.g., large tool or model
|
|
36
|
+
# outputs sent over JSON-RPC). A 100MB limit is large enough for typical
|
|
37
|
+
# interactive use while still protecting the process from unbounded memory
|
|
38
|
+
# growth or buffer-overrun errors when peers send unexpectedly large payloads.
|
|
39
|
+
STDIO_BUFFER_LIMIT = 100 * 1024 * 1024
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WireOverStdio:
|
|
43
|
+
def __init__(self, soul: Soul):
|
|
44
|
+
self._reader: asyncio.StreamReader | None = None
|
|
45
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
46
|
+
|
|
47
|
+
# outward
|
|
48
|
+
self._write_task: asyncio.Task[None] | None = None
|
|
49
|
+
self._write_queue: Queue[JSONRPCOutMessage] = Queue()
|
|
50
|
+
|
|
51
|
+
# inward
|
|
52
|
+
self._dispatch_tasks: set[asyncio.Task[None]] = set()
|
|
53
|
+
|
|
54
|
+
# soul running stuffs
|
|
55
|
+
self._soul = soul
|
|
56
|
+
self._cancel_event: asyncio.Event | None = None
|
|
57
|
+
self._pending_requests: dict[str, Request] = {}
|
|
58
|
+
"""Maps JSON RPC message IDs to pending `Request`s."""
|
|
59
|
+
|
|
60
|
+
async def serve(self) -> None:
|
|
61
|
+
logger.info("Starting Wire server on stdio")
|
|
62
|
+
|
|
63
|
+
self._reader, self._writer = await acp.stdio_streams(limit=STDIO_BUFFER_LIMIT)
|
|
64
|
+
self._write_task = asyncio.create_task(self._write_loop())
|
|
65
|
+
try:
|
|
66
|
+
await self._read_loop()
|
|
67
|
+
finally:
|
|
68
|
+
await self._shutdown()
|
|
69
|
+
|
|
70
|
+
async def _write_loop(self) -> None:
|
|
71
|
+
assert self._writer is not None
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
while True:
|
|
75
|
+
try:
|
|
76
|
+
msg = await self._write_queue.get()
|
|
77
|
+
except QueueShutDown:
|
|
78
|
+
logger.debug("Send queue shut down, stopping Wire server write loop")
|
|
79
|
+
break
|
|
80
|
+
self._writer.write(msg.model_dump_json().encode("utf-8") + b"\n")
|
|
81
|
+
await self._writer.drain()
|
|
82
|
+
except asyncio.CancelledError:
|
|
83
|
+
raise
|
|
84
|
+
except Exception:
|
|
85
|
+
logger.exception("Wire server write loop error:")
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
async def _read_loop(self) -> None:
|
|
89
|
+
assert self._reader is not None
|
|
90
|
+
|
|
91
|
+
while True:
|
|
92
|
+
line = await self._reader.readline()
|
|
93
|
+
if not line:
|
|
94
|
+
logger.info("stdin closed, Wire server exiting")
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
msg = JSONRPCInMessageAdapter.validate_json(line)
|
|
99
|
+
except ValueError:
|
|
100
|
+
logger.error("Invalid JSONRPC line: {line}", line=line)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
task = asyncio.create_task(self._dispatch_msg(msg))
|
|
104
|
+
task.add_done_callback(self._dispatch_tasks.discard)
|
|
105
|
+
self._dispatch_tasks.add(task)
|
|
106
|
+
|
|
107
|
+
async def _shutdown(self) -> None:
|
|
108
|
+
for request in self._pending_requests.values():
|
|
109
|
+
if not request.resolved:
|
|
110
|
+
request.resolve("reject")
|
|
111
|
+
self._pending_requests.clear()
|
|
112
|
+
|
|
113
|
+
if self._cancel_event is not None:
|
|
114
|
+
self._cancel_event.set()
|
|
115
|
+
self._cancel_event = None
|
|
116
|
+
|
|
117
|
+
self._write_queue.shutdown()
|
|
118
|
+
if self._write_task is not None:
|
|
119
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
120
|
+
await self._write_task
|
|
121
|
+
|
|
122
|
+
await asyncio.gather(*self._dispatch_tasks, return_exceptions=True)
|
|
123
|
+
self._dispatch_tasks.clear()
|
|
124
|
+
|
|
125
|
+
if self._writer is not None:
|
|
126
|
+
self._writer.close()
|
|
127
|
+
with contextlib.suppress(Exception):
|
|
128
|
+
await self._writer.wait_closed()
|
|
129
|
+
self._writer = None
|
|
130
|
+
|
|
131
|
+
self._reader = None
|
|
132
|
+
|
|
133
|
+
async def _dispatch_msg(self, msg: JSONRPCInMessage) -> None:
|
|
134
|
+
resp: JSONRPCSuccessResponse | JSONRPCErrorResponse | None = None
|
|
135
|
+
try:
|
|
136
|
+
match msg:
|
|
137
|
+
case JSONRPCPromptMessage():
|
|
138
|
+
resp = await self._handle_prompt(msg)
|
|
139
|
+
case JSONRPCCancelMessage():
|
|
140
|
+
resp = await self._handle_cancel(msg)
|
|
141
|
+
case JSONRPCSuccessResponse() | JSONRPCErrorResponse():
|
|
142
|
+
await self._handle_response(msg)
|
|
143
|
+
|
|
144
|
+
if resp is not None:
|
|
145
|
+
await self._send_msg(resp)
|
|
146
|
+
except Exception:
|
|
147
|
+
logger.exception("Unexpected error dispatching JSONRPC message:")
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
async def _send_msg(self, msg: JSONRPCOutMessage) -> None:
|
|
151
|
+
try:
|
|
152
|
+
await self._write_queue.put(msg)
|
|
153
|
+
except QueueShutDown:
|
|
154
|
+
logger.error("Send queue shut down; dropping message: {msg}", msg=msg)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def _soul_is_running(self) -> bool:
|
|
158
|
+
return self._cancel_event is not None
|
|
159
|
+
|
|
160
|
+
async def _handle_prompt(
|
|
161
|
+
self, msg: JSONRPCPromptMessage
|
|
162
|
+
) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
|
|
163
|
+
if self._soul_is_running:
|
|
164
|
+
# TODO: support queueing multiple inputs
|
|
165
|
+
return JSONRPCErrorResponse(
|
|
166
|
+
id=msg.id,
|
|
167
|
+
error=JSONRPCErrorObject(
|
|
168
|
+
code=ErrorCodes.INVALID_STATE, message="An agent turn is already in progress"
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self._cancel_event = asyncio.Event()
|
|
173
|
+
try:
|
|
174
|
+
await run_soul(
|
|
175
|
+
self._soul,
|
|
176
|
+
msg.params.user_input,
|
|
177
|
+
self._stream_wire_messages,
|
|
178
|
+
self._cancel_event,
|
|
179
|
+
self._soul.wire_file if isinstance(self._soul, KimiSoul) else None,
|
|
180
|
+
)
|
|
181
|
+
return JSONRPCSuccessResponse(
|
|
182
|
+
id=msg.id,
|
|
183
|
+
result={"status": Statuses.FINISHED},
|
|
184
|
+
)
|
|
185
|
+
except LLMNotSet:
|
|
186
|
+
return JSONRPCErrorResponse(
|
|
187
|
+
id=msg.id,
|
|
188
|
+
error=JSONRPCErrorObject(code=ErrorCodes.LLM_NOT_SET, message="LLM is not set"),
|
|
189
|
+
)
|
|
190
|
+
except LLMNotSupported as e:
|
|
191
|
+
return JSONRPCErrorResponse(
|
|
192
|
+
id=msg.id,
|
|
193
|
+
error=JSONRPCErrorObject(code=ErrorCodes.LLM_NOT_SUPPORTED, message=str(e)),
|
|
194
|
+
)
|
|
195
|
+
except ChatProviderError as e:
|
|
196
|
+
return JSONRPCErrorResponse(
|
|
197
|
+
id=msg.id,
|
|
198
|
+
error=JSONRPCErrorObject(code=ErrorCodes.CHAT_PROVIDER_ERROR, message=str(e)),
|
|
199
|
+
)
|
|
200
|
+
except MaxStepsReached as e:
|
|
201
|
+
return JSONRPCSuccessResponse(
|
|
202
|
+
id=msg.id,
|
|
203
|
+
result={"status": Statuses.MAX_STEPS_REACHED, "steps": e.n_steps},
|
|
204
|
+
)
|
|
205
|
+
except RunCancelled:
|
|
206
|
+
return JSONRPCSuccessResponse(
|
|
207
|
+
id=msg.id,
|
|
208
|
+
result={"status": Statuses.CANCELLED},
|
|
209
|
+
)
|
|
210
|
+
finally:
|
|
211
|
+
self._cancel_event = None
|
|
212
|
+
|
|
213
|
+
async def _handle_cancel(
|
|
214
|
+
self, msg: JSONRPCCancelMessage
|
|
215
|
+
) -> JSONRPCSuccessResponse | JSONRPCErrorResponse:
|
|
216
|
+
if not self._soul_is_running:
|
|
217
|
+
return JSONRPCErrorResponse(
|
|
218
|
+
id=msg.id,
|
|
219
|
+
error=JSONRPCErrorObject(
|
|
220
|
+
code=ErrorCodes.INVALID_STATE, message="No agent turn is in progress"
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
assert self._cancel_event is not None
|
|
225
|
+
self._cancel_event.set()
|
|
226
|
+
return JSONRPCSuccessResponse(
|
|
227
|
+
id=msg.id,
|
|
228
|
+
result={},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
async def _handle_response(self, msg: JSONRPCSuccessResponse | JSONRPCErrorResponse) -> None:
|
|
232
|
+
request = self._pending_requests.pop(msg.id, None)
|
|
233
|
+
if request is None:
|
|
234
|
+
logger.error("No pending request for response id={id}", id=msg.id)
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
match request:
|
|
238
|
+
case ApprovalRequest():
|
|
239
|
+
if isinstance(msg, JSONRPCErrorResponse):
|
|
240
|
+
request.resolve("reject")
|
|
241
|
+
else:
|
|
242
|
+
try:
|
|
243
|
+
result = JSONRPCApprovalRequestResult.model_validate(msg.result)
|
|
244
|
+
request.resolve(result.response)
|
|
245
|
+
except pydantic.ValidationError as e:
|
|
246
|
+
logger.error(
|
|
247
|
+
"Invalid response result for request id={id}: {error}",
|
|
248
|
+
id=msg.id,
|
|
249
|
+
error=e,
|
|
250
|
+
)
|
|
251
|
+
request.resolve("reject")
|
|
252
|
+
|
|
253
|
+
async def _stream_wire_messages(self, wire: Wire) -> None:
|
|
254
|
+
wire_ui = wire.ui_side(merge=False)
|
|
255
|
+
while True:
|
|
256
|
+
msg = await wire_ui.receive()
|
|
257
|
+
match msg:
|
|
258
|
+
case ApprovalRequest():
|
|
259
|
+
await self._request_approval(msg)
|
|
260
|
+
case _:
|
|
261
|
+
await self._send_msg(JSONRPCEventMessage(method="event", params=msg))
|
|
262
|
+
|
|
263
|
+
async def _request_approval(self, request: ApprovalRequest) -> None:
|
|
264
|
+
msg_id = request.id # just use the approval request id as message id
|
|
265
|
+
self._pending_requests[msg_id] = request
|
|
266
|
+
await self._send_msg(JSONRPCRequestMessage(id=msg_id, params=request))
|
|
267
|
+
await request.wait()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from kosong.utils.typing import JsonType
|
|
6
|
+
from pydantic import (
|
|
7
|
+
BaseModel,
|
|
8
|
+
ConfigDict,
|
|
9
|
+
TypeAdapter,
|
|
10
|
+
field_serializer,
|
|
11
|
+
field_validator,
|
|
12
|
+
model_serializer,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from kimi_cli.wire.serde import serialize_wire_message
|
|
16
|
+
from kimi_cli.wire.types import (
|
|
17
|
+
ApprovalRequestResolved,
|
|
18
|
+
ContentPart,
|
|
19
|
+
Event,
|
|
20
|
+
Request,
|
|
21
|
+
is_event,
|
|
22
|
+
is_request,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _MessageBase(BaseModel):
|
|
27
|
+
jsonrpc: Literal["2.0"] = "2.0"
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra="forbid")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class JSONRPCEventMessage(_MessageBase):
|
|
33
|
+
method: Literal["event"] = "event"
|
|
34
|
+
params: Event
|
|
35
|
+
|
|
36
|
+
@field_serializer("params")
|
|
37
|
+
def _serialize_params(self, params: Event) -> dict[str, JsonType]:
|
|
38
|
+
return serialize_wire_message(params)
|
|
39
|
+
|
|
40
|
+
@field_validator("params", mode="before")
|
|
41
|
+
@classmethod
|
|
42
|
+
def _validate_params(cls, value: Any) -> Event:
|
|
43
|
+
if is_event(value):
|
|
44
|
+
return value
|
|
45
|
+
raise NotImplementedError("Event message deserialization is not implemented.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JSONRPCRequestMessage(_MessageBase):
|
|
49
|
+
method: Literal["request"] = "request"
|
|
50
|
+
id: str
|
|
51
|
+
params: Request
|
|
52
|
+
|
|
53
|
+
@field_serializer("params")
|
|
54
|
+
def _serialize_params(self, params: Request) -> dict[str, JsonType]:
|
|
55
|
+
return serialize_wire_message(params)
|
|
56
|
+
|
|
57
|
+
@field_validator("params", mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def _validate_params(cls, value: Any) -> Request:
|
|
60
|
+
if is_request(value):
|
|
61
|
+
return value
|
|
62
|
+
raise NotImplementedError("Request message deserialization is not implemented.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class JSONRPCPromptMessage(_MessageBase):
|
|
66
|
+
class Params(BaseModel):
|
|
67
|
+
user_input: str | list[ContentPart]
|
|
68
|
+
|
|
69
|
+
method: Literal["prompt"] = "prompt"
|
|
70
|
+
id: str
|
|
71
|
+
params: Params
|
|
72
|
+
|
|
73
|
+
@model_serializer()
|
|
74
|
+
def _serialize(self) -> dict[str, Any]:
|
|
75
|
+
raise NotImplementedError("Prompt message serialization is not implemented.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class JSONRPCCancelMessage(_MessageBase):
|
|
79
|
+
method: Literal["cancel"] = "cancel"
|
|
80
|
+
id: str
|
|
81
|
+
params: JsonType | None = None
|
|
82
|
+
|
|
83
|
+
@model_serializer()
|
|
84
|
+
def _serialize(self) -> dict[str, Any]:
|
|
85
|
+
raise NotImplementedError("Cancel message serialization is not implemented.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _ResponseBase(_MessageBase):
|
|
89
|
+
id: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class JSONRPCSuccessResponse(_ResponseBase):
|
|
93
|
+
result: JsonType
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class JSONRPCErrorObject(BaseModel):
|
|
97
|
+
code: int
|
|
98
|
+
message: str
|
|
99
|
+
data: JsonType | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class JSONRPCErrorResponse(_ResponseBase):
|
|
103
|
+
error: JSONRPCErrorObject
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class JSONRPCApprovalRequestResult(ApprovalRequestResolved):
|
|
107
|
+
"""
|
|
108
|
+
The `JSONRPCSuccessResponse.result` field for approval request responses should be able to
|
|
109
|
+
be parsed into this type.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
type JSONRPCInMessage = (
|
|
116
|
+
JSONRPCPromptMessage | JSONRPCCancelMessage | JSONRPCSuccessResponse | JSONRPCErrorResponse
|
|
117
|
+
)
|
|
118
|
+
JSONRPCInMessageAdapter = TypeAdapter[JSONRPCInMessage](JSONRPCInMessage)
|
|
119
|
+
|
|
120
|
+
type JSONRPCOutMessage = (
|
|
121
|
+
JSONRPCEventMessage | JSONRPCRequestMessage | JSONRPCSuccessResponse | JSONRPCErrorResponse
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ErrorCodes:
|
|
126
|
+
INVALID_STATE = -32000
|
|
127
|
+
"""The server is in an invalid state to process the request."""
|
|
128
|
+
LLM_NOT_SET = -32001
|
|
129
|
+
"""The LLM is not set."""
|
|
130
|
+
LLM_NOT_SUPPORTED = -32002
|
|
131
|
+
"""The specified LLM is not supported."""
|
|
132
|
+
CHAT_PROVIDER_ERROR = -32003
|
|
133
|
+
"""There was an error from the chat provider."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Statuses:
|
|
137
|
+
FINISHED = "finished"
|
|
138
|
+
"""The agent run has finished successfully."""
|
|
139
|
+
CANCELLED = "cancelled"
|
|
140
|
+
"""The agent run was cancelled by the user."""
|
|
141
|
+
MAX_STEPS_REACHED = "max_steps_reached"
|
|
142
|
+
"""The agent run reached the maximum number of steps."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
WIRE_PROTOCOL_VERSION: str = "1"
|
|
File without changes
|
kimi_cli/utils/aiohttp.py
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
if sys.version_info >= (3, 13):
|
|
7
|
+
QueueShutDown = asyncio.QueueShutDown # type: ignore[assignment]
|
|
8
|
+
|
|
9
|
+
class Queue[T](asyncio.Queue[T]):
|
|
10
|
+
"""Asyncio Queue with shutdown support."""
|
|
11
|
+
|
|
12
|
+
else:
|
|
13
|
+
|
|
14
|
+
class QueueShutDown(Exception):
|
|
15
|
+
"""Raised when operating on a shut down queue."""
|
|
16
|
+
|
|
17
|
+
class _Shutdown:
|
|
18
|
+
"""Sentinel for queue shutdown."""
|
|
19
|
+
|
|
20
|
+
_SHUTDOWN = _Shutdown()
|
|
21
|
+
|
|
22
|
+
class Queue[T](asyncio.Queue[T | _Shutdown]):
|
|
23
|
+
"""Asyncio Queue with shutdown support for Python < 3.13."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self._shutdown = False
|
|
28
|
+
|
|
29
|
+
def shutdown(self, immediate: bool = False) -> None:
|
|
30
|
+
if self._shutdown:
|
|
31
|
+
return
|
|
32
|
+
self._shutdown = True
|
|
33
|
+
if immediate:
|
|
34
|
+
self._queue.clear()
|
|
35
|
+
|
|
36
|
+
getters = list(getattr(self, "_getters", []))
|
|
37
|
+
count = max(1, len(getters))
|
|
38
|
+
self._enqueue_shutdown(count)
|
|
39
|
+
|
|
40
|
+
def _enqueue_shutdown(self, count: int) -> None:
|
|
41
|
+
for _ in range(count):
|
|
42
|
+
try:
|
|
43
|
+
super().put_nowait(_SHUTDOWN)
|
|
44
|
+
except asyncio.QueueFull:
|
|
45
|
+
self._queue.clear()
|
|
46
|
+
super().put_nowait(_SHUTDOWN)
|
|
47
|
+
|
|
48
|
+
async def get(self) -> T:
|
|
49
|
+
if self._shutdown and self.empty():
|
|
50
|
+
raise QueueShutDown
|
|
51
|
+
item = await super().get()
|
|
52
|
+
if isinstance(item, _Shutdown):
|
|
53
|
+
raise QueueShutDown
|
|
54
|
+
return item
|
|
55
|
+
|
|
56
|
+
def get_nowait(self) -> T:
|
|
57
|
+
if self._shutdown and self.empty():
|
|
58
|
+
raise QueueShutDown
|
|
59
|
+
item = super().get_nowait()
|
|
60
|
+
if isinstance(item, _Shutdown):
|
|
61
|
+
raise QueueShutDown
|
|
62
|
+
return item
|
|
63
|
+
|
|
64
|
+
async def put(self, item: T) -> None:
|
|
65
|
+
if self._shutdown:
|
|
66
|
+
raise QueueShutDown
|
|
67
|
+
await super().put(item)
|
|
68
|
+
|
|
69
|
+
def put_nowait(self, item: T) -> None:
|
|
70
|
+
if self._shutdown:
|
|
71
|
+
raise QueueShutDown
|
|
72
|
+
super().put_nowait(item)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from kimi_cli.utils.aioqueue import Queue
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BroadcastQueue[T]:
|
|
7
|
+
"""
|
|
8
|
+
A broadcast queue that allows multiple subscribers to receive published items.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._queues: set[Queue[T]] = set()
|
|
13
|
+
|
|
14
|
+
def subscribe(self) -> Queue[T]:
|
|
15
|
+
"""Create a new subscription queue."""
|
|
16
|
+
queue: Queue[T] = Queue()
|
|
17
|
+
self._queues.add(queue)
|
|
18
|
+
return queue
|
|
19
|
+
|
|
20
|
+
def unsubscribe(self, queue: Queue[T]) -> None:
|
|
21
|
+
"""Remove a subscription queue."""
|
|
22
|
+
self._queues.discard(queue)
|
|
23
|
+
|
|
24
|
+
async def publish(self, item: T) -> None:
|
|
25
|
+
"""Publish an item to all subscription queues."""
|
|
26
|
+
await asyncio.gather(*(queue.put(item) for queue in self._queues))
|
|
27
|
+
|
|
28
|
+
def publish_nowait(self, item: T) -> None:
|
|
29
|
+
"""Publish an item to all subscription queues without waiting."""
|
|
30
|
+
for queue in self._queues:
|
|
31
|
+
queue.put_nowait(item)
|
|
32
|
+
|
|
33
|
+
def shutdown(self, immediate: bool = False) -> None:
|
|
34
|
+
"""Close all subscription queues."""
|
|
35
|
+
for queue in self._queues:
|
|
36
|
+
queue.shutdown(immediate=immediate)
|
|
37
|
+
self._queues.clear()
|
kimi_cli/utils/changelog.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
from typing import NamedTuple
|
|
3
5
|
|
|
@@ -41,13 +43,14 @@ def parse_changelog(md_text: str) -> dict[str, ReleaseEntry]:
|
|
|
41
43
|
|
|
42
44
|
for raw in lines:
|
|
43
45
|
line = raw.rstrip()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
# Format: `## 0.75 (2026-01-09)` or `## Unreleased`
|
|
47
|
+
if line.startswith("## "):
|
|
46
48
|
commit()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
ver = line[3:].strip()
|
|
50
|
+
# Remove trailing date in parentheses if present
|
|
51
|
+
if "(" in ver:
|
|
52
|
+
ver = ver[: ver.find("(")].strip()
|
|
53
|
+
current_ver = ver
|
|
51
54
|
desc_lines = []
|
|
52
55
|
bullet_lines = []
|
|
53
56
|
collecting_desc = True
|
|
@@ -85,7 +88,7 @@ def parse_changelog(md_text: str) -> dict[str, ReleaseEntry]:
|
|
|
85
88
|
return result
|
|
86
89
|
|
|
87
90
|
|
|
88
|
-
def format_release_notes(changelog: dict[str, ReleaseEntry]) -> str:
|
|
91
|
+
def format_release_notes(changelog: dict[str, ReleaseEntry], include_lib_changes: bool) -> str:
|
|
89
92
|
parts: list[str] = []
|
|
90
93
|
for ver, entry in changelog.items():
|
|
91
94
|
s = f"[bold]{ver}[/bold]"
|
|
@@ -93,6 +96,8 @@ def format_release_notes(changelog: dict[str, ReleaseEntry]) -> str:
|
|
|
93
96
|
s += f": {entry.description}"
|
|
94
97
|
if entry.entries:
|
|
95
98
|
for it in entry.entries:
|
|
99
|
+
if it.lower().startswith("lib:") and not include_lib_changes:
|
|
100
|
+
continue
|
|
96
101
|
s += "\n[markdown.item.bullet]• [/]" + it
|
|
97
102
|
parts.append(s + "\n")
|
|
98
103
|
return "\n".join(parts).strip()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_relative_time(timestamp: float) -> str:
|
|
5
|
+
"""Format a timestamp as a relative time string."""
|
|
6
|
+
now = datetime.now()
|
|
7
|
+
dt = datetime.fromtimestamp(timestamp)
|
|
8
|
+
diff = now - dt
|
|
9
|
+
if diff < timedelta(minutes=5):
|
|
10
|
+
return "just now"
|
|
11
|
+
if diff < timedelta(hours=1):
|
|
12
|
+
minutes = int(diff.total_seconds() / 60)
|
|
13
|
+
return f"{minutes}m ago"
|
|
14
|
+
if diff < timedelta(days=1):
|
|
15
|
+
hours = int(diff.total_seconds() / 3600)
|
|
16
|
+
return f"{hours}h ago"
|
|
17
|
+
if diff < timedelta(days=7):
|
|
18
|
+
return f"{diff.days}d ago"
|
|
19
|
+
return dt.strftime("%m-%d")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def format_duration(seconds: int) -> str:
|
|
23
|
+
"""Format a duration in seconds using short units."""
|
|
24
|
+
delta = timedelta(seconds=seconds)
|
|
25
|
+
parts: list[str] = []
|
|
26
|
+
days = delta.days
|
|
27
|
+
if days:
|
|
28
|
+
parts.append(f"{days}d")
|
|
29
|
+
hours, remainder = divmod(delta.seconds, 3600)
|
|
30
|
+
minutes, secs = divmod(remainder, 60)
|
|
31
|
+
if hours:
|
|
32
|
+
parts.append(f"{hours}h")
|
|
33
|
+
if minutes:
|
|
34
|
+
parts.append(f"{minutes}m")
|
|
35
|
+
if secs and not parts:
|
|
36
|
+
parts.append(f"{secs}s")
|
|
37
|
+
return " ".join(parts) or "0s"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from kaos.path import KaosPath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
11
|
+
class Environment:
|
|
12
|
+
os_kind: Literal["Windows", "Linux", "macOS"] | str
|
|
13
|
+
os_arch: str
|
|
14
|
+
os_version: str
|
|
15
|
+
shell_name: Literal["bash", "sh", "Windows PowerShell"]
|
|
16
|
+
shell_path: KaosPath
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
async def detect() -> Environment:
|
|
20
|
+
match platform.system():
|
|
21
|
+
case "Darwin":
|
|
22
|
+
os_kind = "macOS"
|
|
23
|
+
case "Windows":
|
|
24
|
+
os_kind = "Windows"
|
|
25
|
+
case "Linux":
|
|
26
|
+
os_kind = "Linux"
|
|
27
|
+
case system:
|
|
28
|
+
os_kind = system
|
|
29
|
+
|
|
30
|
+
os_arch = platform.machine()
|
|
31
|
+
os_version = platform.version()
|
|
32
|
+
|
|
33
|
+
if os_kind == "Windows":
|
|
34
|
+
shell_name = "Windows PowerShell"
|
|
35
|
+
shell_path = KaosPath("powershell.exe")
|
|
36
|
+
else:
|
|
37
|
+
possible_paths = [
|
|
38
|
+
KaosPath("/bin/bash"),
|
|
39
|
+
KaosPath("/usr/bin/bash"),
|
|
40
|
+
KaosPath("/usr/local/bin/bash"),
|
|
41
|
+
]
|
|
42
|
+
fallback_path = KaosPath("/bin/sh")
|
|
43
|
+
for path in possible_paths:
|
|
44
|
+
if await path.is_file():
|
|
45
|
+
shell_name = "bash"
|
|
46
|
+
shell_path = path
|
|
47
|
+
break
|
|
48
|
+
else:
|
|
49
|
+
shell_name = "sh"
|
|
50
|
+
shell_path = fallback_path
|
|
51
|
+
|
|
52
|
+
return Environment(
|
|
53
|
+
os_kind=os_kind,
|
|
54
|
+
os_arch=os_arch,
|
|
55
|
+
os_version=os_version,
|
|
56
|
+
shell_name=shell_name,
|
|
57
|
+
shell_path=shell_path,
|
|
58
|
+
)
|