kimi-cli 0.45__py3-none-any.whl → 0.46__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 CHANGED
@@ -9,6 +9,17 @@ Internal builds may append content to the Unreleased section.
9
9
  Only write entries that are worth mentioning to users.
10
10
  -->
11
11
 
12
+ ## [0.46] - 2025-11-03
13
+
14
+ ### Added
15
+
16
+ - Introduce Wire over stdio for local IPC (experimental, subject to change)
17
+ - Support Anthropic provider type
18
+
19
+ ### Fixed
20
+
21
+ - Fix binary packed by PyInstaller not working due to wrong entrypoint
22
+
12
23
  ## [0.45] - 2025-10-31
13
24
 
14
25
  ### Added
kimi_cli/app.py CHANGED
@@ -193,3 +193,10 @@ class KimiCLI:
193
193
  with self._app_env():
194
194
  app = ACPServer(self._soul)
195
195
  return await app.run()
196
+
197
+ async def run_wire_server(self) -> bool:
198
+ from kimi_cli.ui.wire import WireServer
199
+
200
+ with self._app_env():
201
+ server = WireServer(self._soul)
202
+ return await server.run()
kimi_cli/cli.py CHANGED
@@ -16,7 +16,7 @@ class Reload(Exception):
16
16
  pass
17
17
 
18
18
 
19
- UIMode = Literal["shell", "print", "acp"]
19
+ UIMode = Literal["shell", "print", "acp", "wire"]
20
20
  InputFormat = Literal["text", "stream-json"]
21
21
  OutputFormat = Literal["text", "stream-json"]
22
22
 
@@ -170,9 +170,12 @@ def kimi(
170
170
 
171
171
  echo: Callable[..., None] = click.echo if verbose else _noop_echo
172
172
 
173
+ if debug:
174
+ logger.enable("kosong")
173
175
  logger.add(
174
176
  get_share_dir() / "logs" / "kimi.log",
175
- level="DEBUG" if debug else "INFO",
177
+ # FIXME: configure level for different modules
178
+ level="TRACE" if debug else "INFO",
176
179
  rotation="06:00",
177
180
  retention="10 days",
178
181
  )
@@ -238,6 +241,10 @@ def kimi(
238
241
  if command is not None:
239
242
  logger.warning("ACP server ignores command argument")
240
243
  return await instance.run_acp_server()
244
+ case "wire":
245
+ if command is not None:
246
+ logger.warning("Wire server ignores command argument")
247
+ return await instance.run_wire_server()
241
248
 
242
249
  while True:
243
250
  try:
kimi_cli/config.py CHANGED
@@ -12,7 +12,7 @@ from kimi_cli.utils.logging import logger
12
12
  class LLMProvider(BaseModel):
13
13
  """LLM provider configuration."""
14
14
 
15
- type: Literal["kimi", "openai_legacy", "openai_responses", "_chaos"]
15
+ type: Literal["kimi", "openai_legacy", "openai_responses", "anthropic", "_chaos"]
16
16
  """Provider type"""
17
17
  base_url: str
18
18
  """API base URL"""
kimi_cli/llm.py CHANGED
@@ -105,6 +105,20 @@ def create_llm(
105
105
  api_key=provider.api_key.get_secret_value(),
106
106
  stream=stream,
107
107
  )
108
+ case "anthropic":
109
+ from kosong.chat_provider.anthropic import Anthropic
110
+
111
+ chat_provider = Anthropic(
112
+ model=model.model,
113
+ base_url=provider.base_url,
114
+ api_key=provider.api_key.get_secret_value(),
115
+ stream=stream,
116
+ default_max_tokens=50000,
117
+ ).with_generation_kwargs(
118
+ # TODO: support configurable values
119
+ thinking={"type": "enabled", "budget_tokens": 1024},
120
+ beta_features=["interleaved-thinking-2025-05-14"],
121
+ )
108
122
  case "_chaos":
109
123
  from kosong.chat_provider.chaos import ChaosChatProvider, ChaosConfig
110
124
 
kimi_cli/soul/toolset.py CHANGED
@@ -2,7 +2,8 @@ from contextvars import ContextVar
2
2
  from typing import override
3
3
 
4
4
  from kosong.base.message import ToolCall
5
- from kosong.tooling import HandleResult, SimpleToolset
5
+ from kosong.tooling import HandleResult
6
+ from kosong.tooling.simple import SimpleToolset
6
7
 
7
8
  current_tool_call = ContextVar[ToolCall | None]("current_tool_call", default=None)
8
9
 
@@ -1,7 +1,8 @@
1
1
  import json
2
2
  from pathlib import Path
3
+ from typing import cast
3
4
 
4
- import streamingjson
5
+ import streamingjson # pyright: ignore[reportMissingTypeStubs]
5
6
  from kosong.utils.typing import JsonType
6
7
 
7
8
  from kimi_cli.utils.string import shorten_middle
@@ -29,15 +30,15 @@ def extract_subtitle(lexer: streamingjson.Lexer, tool_name: str) -> str | None:
29
30
  case "SetTodoList":
30
31
  if not isinstance(curr_args, dict) or not curr_args.get("todos"):
31
32
  return None
32
- if not isinstance(curr_args["todos"], list):
33
+
34
+ from kimi_cli.tools.todo import Params
35
+
36
+ try:
37
+ todo_params = Params.model_validate(curr_args)
38
+ for todo in todo_params.todos:
39
+ subtitle += f"• {todo.title} [{todo.status}]\n"
40
+ except Exception:
33
41
  return None
34
- for todo in curr_args["todos"]:
35
- if not isinstance(todo, dict) or not todo.get("title"):
36
- continue
37
- subtitle += f"• {todo['title']}"
38
- if todo.get("status"):
39
- subtitle += f" [{todo['status']}]"
40
- subtitle += "\n"
41
42
  return "\n" + subtitle.strip()
42
43
  case "Bash":
43
44
  if not isinstance(curr_args, dict) or not curr_args.get("command"):
@@ -72,7 +73,9 @@ def extract_subtitle(lexer: streamingjson.Lexer, tool_name: str) -> str | None:
72
73
  return None
73
74
  subtitle = str(curr_args["url"])
74
75
  case _:
75
- subtitle = "".join(lexer.json_content)
76
+ # lexer.json_content is list[str] based on streamingjson source code
77
+ content: list[str] = cast(list[str], lexer.json_content) # pyright: ignore[reportUnknownMemberType]
78
+ subtitle = "".join(content)
76
79
  if tool_name not in ["SetTodoList"]:
77
80
  subtitle = shorten_middle(subtitle, width=50)
78
81
  return subtitle
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
+ from collections.abc import Callable
2
3
  from pathlib import Path
3
- from typing import override
4
+ from typing import Any, override
4
5
 
5
6
  from kosong.tooling import CallableTool2, ToolReturnType
6
7
  from pydantic import BaseModel, Field
@@ -29,7 +30,7 @@ class Bash(CallableTool2[Params]):
29
30
  description: str = load_desc(Path(__file__).parent / "bash.md", {})
30
31
  params: type[Params] = Params
31
32
 
32
- def __init__(self, approval: Approval, **kwargs):
33
+ def __init__(self, approval: Approval, **kwargs: Any):
33
34
  super().__init__(**kwargs)
34
35
  self._approval = approval
35
36
 
@@ -71,8 +72,13 @@ class Bash(CallableTool2[Params]):
71
72
  )
72
73
 
73
74
 
74
- async def _stream_subprocess(command: str, stdout_cb, stderr_cb, timeout: int) -> int:
75
- async def _read_stream(stream, cb):
75
+ async def _stream_subprocess(
76
+ command: str,
77
+ stdout_cb: Callable[[bytes], None],
78
+ stderr_cb: Callable[[bytes], None],
79
+ timeout: int,
80
+ ) -> int:
81
+ async def _read_stream(stream: asyncio.StreamReader, cb: Callable[[bytes], None]):
76
82
  while True:
77
83
  line = await stream.readline()
78
84
  if line:
@@ -85,6 +91,9 @@ async def _stream_subprocess(command: str, stdout_cb, stderr_cb, timeout: int) -
85
91
  command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
86
92
  )
87
93
 
94
+ assert process.stdout is not None, "stdout is None"
95
+ assert process.stderr is not None, "stderr is None"
96
+
88
97
  try:
89
98
  await asyncio.wait_for(
90
99
  asyncio.gather(
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import override
2
+ from typing import Any, override
3
3
 
4
4
  from kosong.tooling import CallableTool2, ToolError, ToolReturnType
5
5
 
@@ -8,12 +8,12 @@ from kimi_cli.soul.denwarenji import DenwaRenji, DenwaRenjiError, DMail
8
8
  NAME = "SendDMail"
9
9
 
10
10
 
11
- class SendDMail(CallableTool2):
11
+ class SendDMail(CallableTool2[DMail]):
12
12
  name: str = NAME
13
13
  description: str = (Path(__file__).parent / "dmail.md").read_text(encoding="utf-8")
14
14
  params: type[DMail] = DMail
15
15
 
16
- def __init__(self, denwa_renji: DenwaRenji, **kwargs):
16
+ def __init__(self, denwa_renji: DenwaRenji, **kwargs: Any) -> None:
17
17
  super().__init__(**kwargs)
18
18
  self._denwa_renji = denwa_renji
19
19
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  from pathlib import Path
5
- from typing import override
5
+ from typing import Any, override
6
6
 
7
7
  import aiofiles.os
8
8
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
@@ -38,7 +38,7 @@ class Glob(CallableTool2[Params]):
38
38
  )
39
39
  params: type[Params] = Params
40
40
 
41
- def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
41
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs: Any) -> None:
42
42
  super().__init__(**kwargs)
43
43
  self._work_dir = builtin_args.KIMI_WORK_DIR
44
44
 
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  from typing import override
10
10
 
11
11
  import aiohttp
12
- import ripgrepy
12
+ import ripgrepy # pyright: ignore[reportMissingTypeStubs]
13
13
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
14
14
  from pydantic import BaseModel, Field
15
15
 
@@ -1,8 +1,8 @@
1
1
  from pathlib import Path
2
- from typing import override
2
+ from typing import Any, Literal, override
3
3
 
4
4
  import aiofiles
5
- import patch_ng
5
+ import patch_ng # pyright: ignore[reportMissingTypeStubs]
6
6
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
7
7
  from pydantic import BaseModel, Field
8
8
 
@@ -12,6 +12,36 @@ from kimi_cli.tools.file import FileActions
12
12
  from kimi_cli.tools.utils import ToolRejectedError
13
13
 
14
14
 
15
+ def _parse_patch(diff_bytes: bytes) -> patch_ng.PatchSet | None:
16
+ """Parse patch from bytes, returning PatchSet or None on error.
17
+
18
+ This wrapper provides type hints for the untyped patch_ng.fromstring function.
19
+ """
20
+ result: patch_ng.PatchSet | Literal[False] = patch_ng.fromstring(diff_bytes) # pyright: ignore[reportUnknownMemberType]
21
+ return result if result is not False else None
22
+
23
+
24
+ def _count_hunks(patch_set: patch_ng.PatchSet) -> int:
25
+ """Count total hunks across all items in a PatchSet.
26
+
27
+ This wrapper provides type hints for the untyped patch_ng library.
28
+ From source code inspection: PatchSet.items is list[Patch], Patch.hunks is list[Hunk].
29
+ Type ignore needed because patch_ng lacks type annotations.
30
+ """
31
+ items: list[patch_ng.Patch] = patch_set.items # pyright: ignore[reportUnknownMemberType]
32
+ # Each Patch has a hunks attribute (list[Hunk])
33
+ return sum(len(item.hunks) for item in items) # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType]
34
+
35
+
36
+ def _apply_patch(patch_set: patch_ng.PatchSet, root: str) -> bool:
37
+ """Apply a patch to files under the given root directory.
38
+
39
+ This wrapper provides type hints for the untyped patch_ng.apply method.
40
+ """
41
+ success: Any = patch_set.apply(root=root) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
42
+ return bool(success) # pyright: ignore[reportUnknownArgumentType]
43
+
44
+
15
45
  class Params(BaseModel):
16
46
  path: str = Field(description="The absolute path to the file to apply the patch to.")
17
47
  diff: str = Field(description="The diff content in unified format to apply.")
@@ -22,7 +52,7 @@ class PatchFile(CallableTool2[Params]):
22
52
  description: str = (Path(__file__).parent / "patch.md").read_text(encoding="utf-8")
23
53
  params: type[Params] = Params
24
54
 
25
- def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
55
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs: Any):
26
56
  super().__init__(**kwargs)
27
57
  self._work_dir = builtin_args.KIMI_WORK_DIR
28
58
  self._approval = approval
@@ -87,10 +117,10 @@ class PatchFile(CallableTool2[Params]):
87
117
  original_content = await f.read()
88
118
 
89
119
  # Create patch object directly from string (no temporary file needed!)
90
- patch_set = patch_ng.fromstring(params.diff.encode("utf-8"))
120
+ patch_set = _parse_patch(params.diff.encode("utf-8"))
91
121
 
92
- # Handle case where patch_ng.fromstring returns False on parse errors
93
- if not patch_set or patch_set is True:
122
+ # Handle case where parsing failed
123
+ if patch_set is None:
94
124
  return ToolError(
95
125
  message=(
96
126
  "Failed to parse diff content: invalid patch format or no valid hunks found"
@@ -99,7 +129,7 @@ class PatchFile(CallableTool2[Params]):
99
129
  )
100
130
 
101
131
  # Count total hunks across all items
102
- total_hunks = sum(len(item.hunks) for item in patch_set.items)
132
+ total_hunks = _count_hunks(patch_set)
103
133
 
104
134
  if total_hunks == 0:
105
135
  return ToolError(
@@ -108,7 +138,7 @@ class PatchFile(CallableTool2[Params]):
108
138
  )
109
139
 
110
140
  # Apply the patch
111
- success = patch_set.apply(root=str(p.parent))
141
+ success = _apply_patch(patch_set, str(p.parent))
112
142
 
113
143
  if not success:
114
144
  return ToolError(
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import override
2
+ from typing import Any, override
3
3
 
4
4
  import aiofiles
5
5
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
@@ -47,8 +47,9 @@ class ReadFile(CallableTool2[Params]):
47
47
  )
48
48
  params: type[Params] = Params
49
49
 
50
- def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
50
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs: Any) -> None:
51
51
  super().__init__(**kwargs)
52
+
52
53
  self._work_dir = builtin_args.KIMI_WORK_DIR
53
54
 
54
55
  @override
@@ -84,7 +85,7 @@ class ReadFile(CallableTool2[Params]):
84
85
 
85
86
  lines: list[str] = []
86
87
  n_bytes = 0
87
- truncated_line_numbers = []
88
+ truncated_line_numbers: list[int] = []
88
89
  max_lines_reached = False
89
90
  max_bytes_reached = False
90
91
  async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
@@ -108,7 +109,7 @@ class ReadFile(CallableTool2[Params]):
108
109
  break
109
110
 
110
111
  # Format output with line numbers like `cat -n`
111
- lines_with_no = []
112
+ lines_with_no: list[str] = []
112
113
  for line_num, line in zip(
113
114
  range(params.line_offset, params.line_offset + len(lines)), lines, strict=True
114
115
  ):
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import override
2
+ from typing import Any, override
3
3
 
4
4
  import aiofiles
5
5
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
@@ -32,7 +32,7 @@ class StrReplaceFile(CallableTool2[Params]):
32
32
  description: str = (Path(__file__).parent / "replace.md").read_text(encoding="utf-8")
33
33
  params: type[Params] = Params
34
34
 
35
- def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
35
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs: Any):
36
36
  super().__init__(**kwargs)
37
37
  self._work_dir = builtin_args.KIMI_WORK_DIR
38
38
  self._approval = approval
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Literal, override
2
+ from typing import Any, Literal, override
3
3
 
4
4
  import aiofiles
5
5
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
@@ -29,7 +29,7 @@ class WriteFile(CallableTool2[Params]):
29
29
  description: str = (Path(__file__).parent / "write.md").read_text(encoding="utf-8")
30
30
  params: type[Params] = Params
31
31
 
32
- def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
32
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs: Any):
33
33
  super().__init__(**kwargs)
34
34
  self._work_dir = builtin_args.KIMI_WORK_DIR
35
35
  self._approval = approval
kimi_cli/tools/mcp.py CHANGED
@@ -1,12 +1,15 @@
1
+ from typing import Any
2
+
1
3
  import fastmcp
2
4
  import mcp
3
5
  from fastmcp.client.client import CallToolResult
6
+ from fastmcp.client.transports import ClientTransport
4
7
  from kosong.base.message import AudioURLPart, ContentPart, ImageURLPart, TextPart
5
8
  from kosong.tooling import CallableTool, ToolOk, ToolReturnType
6
9
 
7
10
 
8
- class MCPTool(CallableTool):
9
- def __init__(self, mcp_tool: mcp.Tool, client: fastmcp.Client, **kwargs):
11
+ class MCPTool[T: ClientTransport](CallableTool):
12
+ def __init__(self, mcp_tool: mcp.Tool, client: fastmcp.Client[T], **kwargs: Any):
10
13
  super().__init__(
11
14
  name=mcp_tool.name,
12
15
  description=mcp_tool.description or "",
@@ -16,7 +19,7 @@ class MCPTool(CallableTool):
16
19
  self._mcp_tool = mcp_tool
17
20
  self._client = client
18
21
 
19
- async def __call__(self, *args, **kwargs) -> ToolReturnType:
22
+ async def __call__(self, *args: Any, **kwargs: Any) -> ToolReturnType:
20
23
  async with self._client as client:
21
24
  result = await client.call_tool(self._mcp_tool.name, kwargs, timeout=20)
22
25
  return convert_tool_result(result)
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  from pathlib import Path
3
- from typing import override
3
+ from typing import Any, override
4
4
 
5
5
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
6
6
  from pydantic import BaseModel, Field
@@ -49,7 +49,7 @@ class Task(CallableTool2[Params]):
49
49
  name: str = "Task"
50
50
  params: type[Params] = Params
51
51
 
52
- def __init__(self, agent_spec: ResolvedAgentSpec, runtime: Runtime, **kwargs):
52
+ def __init__(self, agent_spec: ResolvedAgentSpec, runtime: Runtime, **kwargs: Any):
53
53
  super().__init__(
54
54
  description=load_desc(
55
55
  Path(__file__).parent / "task.md",
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import override
2
+ from typing import Any, override
3
3
 
4
4
  from kosong.tooling import CallableTool2, ToolReturnType
5
5
  from pydantic import BaseModel, Field, ValidationError
@@ -39,7 +39,7 @@ class SearchWeb(CallableTool2[Params]):
39
39
  description: str = load_desc(Path(__file__).parent / "search.md", {})
40
40
  params: type[Params] = Params
41
41
 
42
- def __init__(self, config: Config, **kwargs):
42
+ def __init__(self, config: Config, **kwargs: Any):
43
43
  super().__init__(**kwargs)
44
44
  if config.services.moonshot_search is not None:
45
45
  self._base_url = config.services.moonshot_search.base_url
@@ -0,0 +1,109 @@
1
+ # Wire over STDIO
2
+
3
+ Learn how `WireServer` (src/kimi_cli/ui/wire/__init__.py) exposes the Soul runtime over stdio.
4
+ Use this reference when building clients or SDKs.
5
+
6
+ ## Transport
7
+ - The server acquires stdio streams via `acp.stdio_streams()` and stays alive until stdin closes.
8
+ - Messages use newline-delimited JSON. Each object must include `"jsonrpc": "2.0"`.
9
+ - Outbound JSON is UTF-8 encoded with compact separators `(",", ":")`.
10
+
11
+ ## Lifecycle
12
+ 1. A client launches `kimi` (or another entry point) with the wire UI enabled.
13
+ 2. `WireServer.run()` spawns a reader loop on stdin and a writer loop draining an internal queue.
14
+ 3. Incoming payloads are validated by `JSONRPC_MESSAGE_ADAPTER`; invalid objects only log warnings.
15
+ 4. The Soul uses `Wire` (src/kimi_cli/wire/__init__.py); the UI forwards every message as JSON-RPC.
16
+ 5. EOF on stdin or a fatal error cancels the Soul, rejects approvals, and closes stdout.
17
+
18
+ ## Client → Server calls
19
+
20
+ ### `run`
21
+ - Request:
22
+ ```json
23
+ {"jsonrpc": "2.0", "id": "<request-id>", "method": "run", "params": {"input": "<prompt>"}}
24
+ ```
25
+ `params.prompt` is accepted as an alias for `params.input`.
26
+ - Success results:
27
+ - `{"status": "finished"}` when the run completes.
28
+ - `{"status": "cancelled"}` when either side interrupts.
29
+ - `{"status": "max_steps_reached", "steps": <int>}` when the step limit triggers.
30
+ - Error codes:
31
+ - `-32000`: A run is already in progress.
32
+ - `-32602`: The `input` or `prompt` parameter is missing or not a string.
33
+ - `-32001`: LLM is not configured.
34
+ - `-32002`: The chat provider reported an error.
35
+ - `-32003`: The requested LLM is unsupported.
36
+ - `-32099`: An unhandled exception occurred during the run.
37
+
38
+ ### `interrupt`
39
+ - Request:
40
+ ```json
41
+ {"jsonrpc": "2.0", "id": "<request-id>", "method": "interrupt", "params": {}}
42
+ ```
43
+ The `id` field is optional; omitting it turns the request into a notification.
44
+ - Success results:
45
+ - `{"status": "ok"}` when a running Soul acknowledges the interrupt.
46
+ - `{"status": "idle"}` when no run is active.
47
+ - Interrupt requests never raise protocol errors.
48
+
49
+ ## Server → Client traffic
50
+
51
+ ### Event notifications
52
+ Events are JSON-RPC notifications with method `event` and no `id`.
53
+ Payloads come from `serialize_event` (src/kimi_cli/wire/message.py):
54
+ - `step_begin`: payload `{"n": <int>}` with the 1-based step counter.
55
+ - `step_interrupted`: no payload; the Soul paused mid-step.
56
+ - `compaction_begin`: no payload; a compaction pass started.
57
+ - `compaction_end`: no payload; always follows `compaction_begin`.
58
+ - `status_update`: payload `{"context_usage": <int>}` from `StatusSnapshot`.
59
+ - `content_part`: JSON object produced by `ContentPart.model_dump(mode="json", exclude_none=True)`.
60
+ - `tool_call`: JSON object produced by `ToolCall.model_dump(mode="json", exclude_none=True)`.
61
+ - `tool_call_part`: JSON object from `ToolCallPart.model_dump(mode="json", exclude_none=True)`.
62
+ - `tool_result`: object with `tool_call_id`, `ok`, and `result` (`output`, `message`, `brief`).
63
+ When `ok` is true the `output` may be text, a JSON object, or an array of JSON objects for
64
+ multi-part content.
65
+
66
+ Event order mirrors Soul execution because the server uses an `asyncio.Queue` for FIFO delivery.
67
+
68
+ ### Approval requests
69
+ - Approval prompts use method `request`; their `id` equals the UUID in `ApprovalRequest.id`:
70
+ ```json
71
+ {
72
+ "jsonrpc": "2.0",
73
+ "id": "<approval-id>",
74
+ "method": "request",
75
+ "params": {
76
+ "type": "approval",
77
+ "payload": {
78
+ "id": "<approval-id>",
79
+ "tool_call_id": "<tool-call-id>",
80
+ "sender": "<agent>",
81
+ "action": "<action>",
82
+ "description": "<human readable context>"
83
+ }
84
+ }
85
+ }
86
+ ```
87
+ - Clients reply with JSON-RPC success.
88
+ `result.response` must be `approve`, `approve_for_session`, or `reject`:
89
+ ```json
90
+ {"jsonrpc": "2.0", "id": "<approval-id>", "result": {"response": "approve"}}
91
+ ```
92
+ - Error responses or unknown values are interpreted as rejection.
93
+ - Unanswered approvals are auto-rejected during server shutdown.
94
+
95
+ ## Error responses from the server
96
+ Errors follow JSON-RPC semantics.
97
+ The error object includes `code` and `message`.
98
+ Custom codes live in the `-320xx` range.
99
+ Clients should allow an optional `data` field even though the server omits it today.
100
+
101
+ ## Shutdown semantics
102
+ - Shutdown cancels runs, stops the writer queue, rejects pending approvals, and closes stdout.
103
+ - EOF on stdout signals process exit; clients can treat it as terminal.
104
+
105
+ ## Implementation notes for SDK authors
106
+ - Only one `run` call may execute at a time; queue additional runs client side.
107
+ - The payloads for `content_part`, `tool_call`, and `tool_call_part` already contain JSON objects.
108
+ - Approval handling is synchronous; always send a response even if the user cancels.
109
+ - Logging is verbose for non-stream messages; unknown methods are ignored for forward compatibility.
@@ -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
+ ]
kimi_cli/wire/message.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import asyncio
2
2
  import uuid
3
+ from collections.abc import Sequence
3
4
  from enum import Enum
4
- from typing import TYPE_CHECKING, NamedTuple
5
+ from typing import TYPE_CHECKING, Any, NamedTuple
5
6
 
6
7
  from kosong.base.message import ContentPart, ToolCall, ToolCallPart
7
- from kosong.tooling import ToolResult
8
+ from kosong.tooling import ToolOk, ToolResult
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from kimi_cli.soul import StatusSnapshot
@@ -89,3 +90,89 @@ class ApprovalRequest:
89
90
 
90
91
 
91
92
  type WireMessage = Event | ApprovalRequest
93
+
94
+
95
+ def serialize_event(event: Event) -> dict[str, Any]:
96
+ """
97
+ Convert an event message into a JSON-serializable dictionary.
98
+ """
99
+ match event:
100
+ case StepBegin():
101
+ return {"type": "step_begin", "payload": {"n": event.n}}
102
+ case StepInterrupted():
103
+ return {"type": "step_interrupted"}
104
+ case CompactionBegin():
105
+ return {"type": "compaction_begin"}
106
+ case CompactionEnd():
107
+ return {"type": "compaction_end"}
108
+ case StatusUpdate():
109
+ return {
110
+ "type": "status_update",
111
+ "payload": {"context_usage": event.status.context_usage},
112
+ }
113
+ case ContentPart():
114
+ return {
115
+ "type": "content_part",
116
+ "payload": event.model_dump(mode="json", exclude_none=True),
117
+ }
118
+ case ToolCall():
119
+ return {
120
+ "type": "tool_call",
121
+ "payload": event.model_dump(mode="json", exclude_none=True),
122
+ }
123
+ case ToolCallPart():
124
+ return {
125
+ "type": "tool_call_part",
126
+ "payload": event.model_dump(mode="json", exclude_none=True),
127
+ }
128
+ case ToolResult():
129
+ return {
130
+ "type": "tool_result",
131
+ "payload": serialize_tool_result(event),
132
+ }
133
+
134
+
135
+ def serialize_approval_request(request: ApprovalRequest) -> dict[str, Any]:
136
+ """
137
+ Convert an ApprovalRequest into a JSON-serializable dictionary.
138
+ """
139
+ return {
140
+ "id": request.id,
141
+ "tool_call_id": request.tool_call_id,
142
+ "sender": request.sender,
143
+ "action": request.action,
144
+ "description": request.description,
145
+ }
146
+
147
+
148
+ def serialize_tool_result(result: ToolResult) -> dict[str, Any]:
149
+ if isinstance(result.result, ToolOk):
150
+ ok = True
151
+ result_data = {
152
+ "output": _serialize_tool_output(result.result.output),
153
+ "message": result.result.message,
154
+ "brief": result.result.brief,
155
+ }
156
+ else:
157
+ ok = False
158
+ result_data = {
159
+ "output": result.result.output,
160
+ "message": result.result.message,
161
+ "brief": result.result.brief,
162
+ }
163
+ return {
164
+ "tool_call_id": result.tool_call_id,
165
+ "ok": ok,
166
+ "result": result_data,
167
+ }
168
+
169
+
170
+ def _serialize_tool_output(
171
+ output: str | ContentPart | Sequence[ContentPart],
172
+ ) -> str | list[Any] | dict[str, Any]:
173
+ if isinstance(output, str):
174
+ return output
175
+ elif isinstance(output, ContentPart):
176
+ return output.model_dump(mode="json", exclude_none=True)
177
+ else: # Sequence[ContentPart]
178
+ return [part.model_dump(mode="json", exclude_none=True) for part in output]
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kimi-cli
3
- Version: 0.45
3
+ Version: 0.46
4
4
  Summary: Kimi CLI is your next CLI agent.
5
5
  Requires-Dist: agent-client-protocol==0.6.2
6
6
  Requires-Dist: aiofiles==25.1.0
7
7
  Requires-Dist: aiohttp==3.13.2
8
8
  Requires-Dist: click==8.3.0
9
- Requires-Dist: kosong==0.16.2
9
+ Requires-Dist: kosong==0.17.0
10
10
  Requires-Dist: loguru==0.7.3
11
11
  Requires-Dist: patch-ng==1.19.0
12
12
  Requires-Dist: prompt-toolkit==3.0.52
@@ -29,6 +29,7 @@ Description-Content-Type: text/markdown
29
29
  [![Checks](https://img.shields.io/github/check-runs/MoonshotAI/kimi-cli/main)](https://github.com/MoonshotAI/kimi-cli/actions)
30
30
  [![Version](https://img.shields.io/pypi/v/kimi-cli)](https://pypi.org/project/kimi-cli/)
31
31
  [![Downloads](https://img.shields.io/pypi/dw/kimi-cli)](https://pypistats.org/packages/kimi-cli)
32
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/MoonshotAI/kimi-cli)
32
33
 
33
34
  [中文](https://www.kimi.com/coding/docs/kimi-cli.html)
34
35
 
@@ -1,15 +1,15 @@
1
- kimi_cli/CHANGELOG.md,sha256=0affdbd19f99b6dedb4cc2cac63eb2f9f942d8cfab44f01b445374764b572a10,8603
1
+ kimi_cli/CHANGELOG.md,sha256=cd7f214ad9fb6cdc9af293cc90d69aeba9c24b4dc878486a99187de70ff197d2,8832
2
2
  kimi_cli/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
3
3
  kimi_cli/agents/default/agent.yaml,sha256=6e5c51987ef5cfc0c4c4e34cc20b6fc975953ee219623fccae81a19155aab7b3,709
4
4
  kimi_cli/agents/default/sub.yaml,sha256=e0c1ea34fdb04b0d6dc635709f0f130aff25d7f9fb97e238470143c8145be251,634
5
5
  kimi_cli/agents/default/system.md,sha256=1d8fd4956b2442215396b5e9651771c9da8f9505ccbd3b6d5e91b1ac4ff35418,5001
6
6
  kimi_cli/agentspec.py,sha256=1148b5184ca610b2fb261ce365a63eb2fc9d09497330fe0ea4b2567fc98d5657,4307
7
- kimi_cli/app.py,sha256=34fde6911d3b4fa6ada013f5ae714cdf354be479da9adb26c6460de0a9ba10c3,7053
8
- kimi_cli/cli.py,sha256=5731276d9c13e58962c39cdf9db3a57bd875a7a09d560c64498454df62b717a0,6910
9
- kimi_cli/config.py,sha256=5524c2dd7d3d10e7965901253c754185a4e4b1d30937598ff2b321a070c760be,5012
7
+ kimi_cli/app.py,sha256=f7a6a39300df101504882121d3930d6a8660e0f4182421aac456fe3fa684e560,7260
8
+ kimi_cli/cli.py,sha256=54bd72907fc99feea96bbaf9eeb808a2a21a2f0c0c6b5d11229a4263597a5920,7215
9
+ kimi_cli/config.py,sha256=85ce4877f0f05f72e2f390b6ebb6e8be5525a719ea4a6e3560817527c5261fb8,5025
10
10
  kimi_cli/constant.py,sha256=78e25b9304cca7b6f70bb08bf0e1fee4b066297a05386e56dd6935ba42027cd9,110
11
11
  kimi_cli/exception.py,sha256=a3fec07566da7d2d34be8cc454fb825f34109bbde3cddf69e1ece6ab21b4b219,259
12
- kimi_cli/llm.py,sha256=fdb4f944d6ccec9fe5b8bc1f80bb283df629663659fcdbffe405af2f61f73733,4442
12
+ kimi_cli/llm.py,sha256=a1c3537a427c0c3ace75bbc0cebf3749215562750570113078a1fcc9e9c80a86,5026
13
13
  kimi_cli/metadata.py,sha256=9e9d4bc12ff26fc34e0e09d9068be989f2ff3c8b682ef453de69e442f8f672a1,1557
14
14
  kimi_cli/prompts/__init__.py,sha256=6dc5ed2d841f145c09550075f30853cdc51f00d2f5d9aa1097f8edea770536e7,174
15
15
  kimi_cli/prompts/compact.md,sha256=6655bd7d8270b24d8f97b51ef7c471cf71d686c56f8ec9a5cc9e47caa3aae87c,1877
@@ -26,27 +26,27 @@ kimi_cli/soul/denwarenji.py,sha256=66b95f052a1fa844e2347972d34e1916a7be24d3e4937
26
26
  kimi_cli/soul/kimisoul.py,sha256=68c65fe0f97700832a42018ae00892d38dc79f14759265042f47b707dbc0c749,11822
27
27
  kimi_cli/soul/message.py,sha256=7a52a6d4d63ef1a3621d93d5ff86887baa7e67019bf2e9a08c374fc130b8d152,2541
28
28
  kimi_cli/soul/runtime.py,sha256=9421a3ce6882587a95ecdf77b3d75f3b7ecab55cf68dc57e3e563b93e5a02e46,2690
29
- kimi_cli/soul/toolset.py,sha256=60166d89ef0efac690fa6866e88afe70fbe80ad862ba2524d70ddf657a730d14,744
30
- kimi_cli/tools/__init__.py,sha256=4d612402814eede7182e0a55e7dd21c4532b5dd44700dc744763a8308c2f74f8,3280
31
- kimi_cli/tools/bash/__init__.py,sha256=de21b19c714bda53f6c89e3348c4c82fb4278040130fed1d261b3ab203054e8c,3028
29
+ kimi_cli/soul/toolset.py,sha256=34d3cb269cb28f2ca1a2323d984625e2ee822d124789a5fcf63651bddf2102fe,777
30
+ kimi_cli/tools/__init__.py,sha256=3b0c7446947219933d0b6819c45b8b57befb8e498171b3374720925697b8c11e,3430
31
+ kimi_cli/tools/bash/__init__.py,sha256=e7821ce9296171c538117398d77311d220a8000de625302f9b8faf62de5910a5,3304
32
32
  kimi_cli/tools/bash/bash.md,sha256=5d9cc54b3718097951340b0a737c8e1fa308341fd2c4ebd121be94de31dd5f73,2348
33
- kimi_cli/tools/dmail/__init__.py,sha256=a9186ed4e52c34cab7516060bb6edca73324912233b1581c0d3a40b026400133,1277
33
+ kimi_cli/tools/dmail/__init__.py,sha256=dfc9ceccb5a47211ab99b7afa4624d13bf147ef68e2a079867f93cf46dfb58d2,1302
34
34
  kimi_cli/tools/dmail/dmail.md,sha256=0d18cae387dd52127ddc99e296253c09e68ccba5f343153c0adbe77d7586e759,1912
35
35
  kimi_cli/tools/file/__init__.py,sha256=1516fb4c71097f9c14b605e7b9a1872af8786bdcb48323d1fa83bb1419436abb,546
36
36
  kimi_cli/tools/file/glob.md,sha256=11fbfaf6033f57b69c6f91077ddd90505036937cd7217600d96992b9a48b7ca7,1400
37
- kimi_cli/tools/file/glob.py,sha256=67b0b55e95b57e34cb43ecf382667d1b9b78903787c00bb2d0929dbede812f2f,5413
37
+ kimi_cli/tools/file/glob.py,sha256=a95f0f502fe6458f8c21a7aa5f15e27828993b05fed83b83f8a7c0dc9c50a48b,5431
38
38
  kimi_cli/tools/file/grep.md,sha256=12353db42cf3b5d9d91ac7c0f0b9c2a732e8b050c23a78f4e668db823cca4d50,245
39
- kimi_cli/tools/file/grep.py,sha256=01794320049b46207aeb28bd7f3dbea373ede891916107cab871ea959e81a335,9787
39
+ kimi_cli/tools/file/grep.py,sha256=77c8ae701c2d827bc5784e140fb007c4153162b278803956915858064f8733e5,9830
40
40
  kimi_cli/tools/file/patch.md,sha256=f9edbed6c6a03bf7448a51acc1f42622395fd7f1674a814e9a3b2302d3b7b92e,404
41
- kimi_cli/tools/file/patch.py,sha256=7e31d85ece4ece2439e9dfd085505e496d9e47d176b820c61798e3c4f71628db,5228
41
+ kimi_cli/tools/file/patch.py,sha256=6ee74656c611510605cc2a70e32b8ed291163e79c458b06fd5a310d48650359f,6632
42
42
  kimi_cli/tools/file/read.md,sha256=4c2d83e557daadc0612fb1a613e252b2c8e4df7ae57e6db094e9e75e994cb23b,1066
43
- kimi_cli/tools/file/read.py,sha256=1d3d8116ee23f224d04c5ba10e3c3261e94fbbe3b932d57ae5fca48a0afdb592,5068
43
+ kimi_cli/tools/file/read.py,sha256=76ea260a813a987a4e34706f6cc3c2b77fc3f31a76831883caa91f9ab86df6a6,5109
44
44
  kimi_cli/tools/file/replace.md,sha256=f429f263fa580f2f6107907a33ba8800dcdbd4fc1d9ff8dc4f858bd76ec6bbc6,267
45
- kimi_cli/tools/file/replace.py,sha256=8df993fb49a975b87820fbdbc7d7b349e18516250b0f895dd047b87c3df89e73,5204
45
+ kimi_cli/tools/file/replace.py,sha256=c6c83869bc1431c645de52c920713e38970668fc531ee005680317a0dfeb4095,5214
46
46
  kimi_cli/tools/file/write.md,sha256=f37b0f4742da57797ec4dd29fbd4fdc9b6617c6be644724a3b16d651c6129cec,324
47
- kimi_cli/tools/file/write.py,sha256=22f4eb8e635b279e48bde7f09e7416a01b8ed88c140424c149a6398a0a321f1a,4378
48
- kimi_cli/tools/mcp.py,sha256=12f63c9ee5b82a5b0f23daca0b5ce4ceb3a6190ce5b553ee24e499699521e426,3620
49
- kimi_cli/tools/task/__init__.py,sha256=193ad32d83e6da7991416b845be5a31cc64657c0945872acfc087058dd1d7b37,6566
47
+ kimi_cli/tools/file/write.py,sha256=a7b48ae20ba6e8e6577ce30bc8561c74d0e76d4cebaa107bdcafb7611eae9dfc,4388
48
+ kimi_cli/tools/mcp.py,sha256=4e8b71c316a5d752ad05ee14af3a3db8ba433b7cc0a2231de4e1a03d4cf9365c,3736
49
+ kimi_cli/tools/task/__init__.py,sha256=7bd6bf96a40d0e2456079f56e2854f71263ad8844265be628753b5eec4a34d5c,6576
50
50
  kimi_cli/tools/task/task.md,sha256=391cc3553c7d310a323626bae180dd41cb810fb1233583713ebde105f954147a,2280
51
51
  kimi_cli/tools/test.py,sha256=c094a91a2d1a5259e192f1147facd5eebd5e5c413787fce167db90e4b41b5119,1442
52
52
  kimi_cli/tools/think/__init__.py,sha256=31b06088e2404cb09d42e0acec97c185e4861890bb687f28b41f39cea01b5733,603
@@ -58,7 +58,7 @@ kimi_cli/tools/web/__init__.py,sha256=e13108c598828a8a05907a7a821e7ac92f5d63572b
58
58
  kimi_cli/tools/web/fetch.md,sha256=56d00bd93b4e379c4f7efe445fce963eb26b8d20f85d4c19097ba6f33bd0019a,67
59
59
  kimi_cli/tools/web/fetch.py,sha256=66448121d27d67f75b977c32244c721c2ccff1b2e097c2fe6717e66018d8f747,3183
60
60
  kimi_cli/tools/web/search.md,sha256=24049f9e90d37083e0fc78b8b2e3a5f6fadf09bea00f36712b235d1212a2f532,146
61
- kimi_cli/tools/web/search.py,sha256=85de343b20bc9e58de8a09aba7c3aac619d6fc6d30a6a5b565108faeb4500faf,4517
61
+ kimi_cli/tools/web/search.py,sha256=5a5a3b7b160d6bd41f99419f7b7ae346f9dd29d37b8dbbe2dad912763d462554,4527
62
62
  kimi_cli/ui/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
63
63
  kimi_cli/ui/acp/__init__.py,sha256=a35e17273e943168882865f90d922180c5a1f01de0d128c899ffcfe55a9db3c3,17120
64
64
  kimi_cli/ui/print/__init__.py,sha256=ca402bec701a253acd6de5b57d59635ac0b05d4013cebc26877c5aa4aa2c27c7,5546
@@ -73,6 +73,9 @@ kimi_cli/ui/shell/replay.py,sha256=e54f58acebc46ad944e1a2cdf54d81559262d2cf8baf5
73
73
  kimi_cli/ui/shell/setup.py,sha256=8fbf2935fc5b972d2c3946e8dc9f4a7e9d2953810b57c0fb6f22172abf3e6fb5,5369
74
74
  kimi_cli/ui/shell/update.py,sha256=56dcb0bd1da82b98c22bfdddca717a2805bd8ac3e93bf23fb3b508549c41fae8,7340
75
75
  kimi_cli/ui/shell/visualize.py,sha256=1ca7a1e766f96f108318640d48552111d42b433e4ddb551af869d331c074d950,4112
76
+ kimi_cli/ui/wire/README.md,sha256=0d9beaf38d4f4f1777013e85445e615fcacfe3a169d11151c42a141dec6196bd,4849
77
+ kimi_cli/ui/wire/__init__.py,sha256=9252f53cd32b5a114afdbc402817a627818a48897554977908b5c5e64864801e,11932
78
+ kimi_cli/ui/wire/jsonrpc.py,sha256=c093a540e70026cb48eae494cf4c8ad04f1180338eaaad0a23d64798bb679944,995
76
79
  kimi_cli/utils/aiohttp.py,sha256=f8f61e3beaf6439e949c33c3a10db3035bf88136e882b09c858ea92a4c888e00,245
77
80
  kimi_cli/utils/changelog.py,sha256=bfcf5a5a360b13648bb7a6abc83e427270caa502646b5acc950d62148510641c,3402
78
81
  kimi_cli/utils/logging.py,sha256=129298ac214ecd8d913c3431cc05d754f9c4c8c4042c458618bf9e8ddebdb763,399
@@ -82,8 +85,8 @@ kimi_cli/utils/pyinstaller.py,sha256=e5d709d0490ef8645bbed2d2363920c59f25bd17c04
82
85
  kimi_cli/utils/signals.py,sha256=20e0d158a1043189d44815fe3624cd0bfe41e99620a18ac9e12c0eda6db5220f,1387
83
86
  kimi_cli/utils/string.py,sha256=0d437d3633199df1051813af8b49a2f808c6525547310cc5c3d427710d2eae06,593
84
87
  kimi_cli/wire/__init__.py,sha256=9f1d7eb58f76885edaf76f769371c363ec801b46cada03883eeb3536fa2677f7,1896
85
- kimi_cli/wire/message.py,sha256=72222d3f3d7228a323dbba7b1084f35018104c58e4bb2aa51d0827984791841d,2398
86
- kimi_cli-0.45.dist-info/WHEEL,sha256=70ab3c2925fe316809860cb034f99ba13c4b49819b339959274aab755cc084a8,78
87
- kimi_cli-0.45.dist-info/entry_points.txt,sha256=97e051756296e9db3167f6dce61d6c88e58d170314a2d63d18c84c73a5c1333b,44
88
- kimi_cli-0.45.dist-info/METADATA,sha256=bac942d1519d1a5f2650199cb6add467e99274392d3e9b839707915a1acd58d0,5217
89
- kimi_cli-0.45.dist-info/RECORD,,
88
+ kimi_cli/wire/message.py,sha256=005213250d05c3c724a5eac6f3c1c3843117439c5e5a24619e9570f59986f67e,5201
89
+ kimi_cli-0.46.dist-info/WHEEL,sha256=70ab3c2925fe316809860cb034f99ba13c4b49819b339959274aab755cc084a8,78
90
+ kimi_cli-0.46.dist-info/entry_points.txt,sha256=97e051756296e9db3167f6dce61d6c88e58d170314a2d63d18c84c73a5c1333b,44
91
+ kimi_cli-0.46.dist-info/METADATA,sha256=0c527326fc9fdbbb9bfd7cabeba9cd0004add9a00082ae8594e350ab90904549,5309
92
+ kimi_cli-0.46.dist-info/RECORD,,