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.
Files changed (88) hide show
  1. kimi_cli/CHANGELOG.md +165 -0
  2. kimi_cli/__init__.py +0 -374
  3. kimi_cli/agents/{koder → default}/agent.yaml +1 -1
  4. kimi_cli/agents/{koder → default}/system.md +1 -1
  5. kimi_cli/agentspec.py +115 -0
  6. kimi_cli/app.py +208 -0
  7. kimi_cli/cli.py +321 -0
  8. kimi_cli/config.py +33 -16
  9. kimi_cli/constant.py +4 -0
  10. kimi_cli/exception.py +16 -0
  11. kimi_cli/llm.py +144 -3
  12. kimi_cli/metadata.py +6 -69
  13. kimi_cli/prompts/__init__.py +4 -0
  14. kimi_cli/session.py +103 -0
  15. kimi_cli/soul/__init__.py +130 -9
  16. kimi_cli/soul/agent.py +159 -0
  17. kimi_cli/soul/approval.py +5 -6
  18. kimi_cli/soul/compaction.py +106 -0
  19. kimi_cli/soul/context.py +1 -1
  20. kimi_cli/soul/kimisoul.py +180 -80
  21. kimi_cli/soul/message.py +6 -6
  22. kimi_cli/soul/runtime.py +96 -0
  23. kimi_cli/soul/toolset.py +3 -2
  24. kimi_cli/tools/__init__.py +35 -31
  25. kimi_cli/tools/bash/__init__.py +25 -9
  26. kimi_cli/tools/bash/cmd.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +5 -4
  28. kimi_cli/tools/file/__init__.py +8 -0
  29. kimi_cli/tools/file/glob.md +1 -1
  30. kimi_cli/tools/file/glob.py +4 -4
  31. kimi_cli/tools/file/grep.py +36 -19
  32. kimi_cli/tools/file/patch.py +52 -10
  33. kimi_cli/tools/file/read.py +6 -5
  34. kimi_cli/tools/file/replace.py +16 -4
  35. kimi_cli/tools/file/write.py +16 -4
  36. kimi_cli/tools/mcp.py +7 -4
  37. kimi_cli/tools/task/__init__.py +60 -41
  38. kimi_cli/tools/task/task.md +1 -1
  39. kimi_cli/tools/todo/__init__.py +4 -2
  40. kimi_cli/tools/utils.py +1 -1
  41. kimi_cli/tools/web/fetch.py +2 -1
  42. kimi_cli/tools/web/search.py +13 -12
  43. kimi_cli/ui/__init__.py +0 -68
  44. kimi_cli/ui/acp/__init__.py +67 -38
  45. kimi_cli/ui/print/__init__.py +46 -69
  46. kimi_cli/ui/shell/__init__.py +145 -154
  47. kimi_cli/ui/shell/console.py +27 -1
  48. kimi_cli/ui/shell/debug.py +187 -0
  49. kimi_cli/ui/shell/keyboard.py +183 -0
  50. kimi_cli/ui/shell/metacmd.py +34 -81
  51. kimi_cli/ui/shell/prompt.py +245 -28
  52. kimi_cli/ui/shell/replay.py +104 -0
  53. kimi_cli/ui/shell/setup.py +19 -19
  54. kimi_cli/ui/shell/update.py +11 -5
  55. kimi_cli/ui/shell/visualize.py +576 -0
  56. kimi_cli/ui/wire/README.md +109 -0
  57. kimi_cli/ui/wire/__init__.py +340 -0
  58. kimi_cli/ui/wire/jsonrpc.py +48 -0
  59. kimi_cli/utils/__init__.py +0 -0
  60. kimi_cli/utils/aiohttp.py +10 -0
  61. kimi_cli/utils/changelog.py +6 -2
  62. kimi_cli/utils/clipboard.py +10 -0
  63. kimi_cli/utils/message.py +15 -1
  64. kimi_cli/utils/rich/__init__.py +33 -0
  65. kimi_cli/utils/rich/markdown.py +959 -0
  66. kimi_cli/utils/rich/markdown_sample.md +108 -0
  67. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  68. kimi_cli/utils/signals.py +41 -0
  69. kimi_cli/utils/string.py +8 -0
  70. kimi_cli/utils/term.py +114 -0
  71. kimi_cli/wire/__init__.py +73 -0
  72. kimi_cli/wire/message.py +191 -0
  73. kimi_cli-0.52.dist-info/METADATA +186 -0
  74. kimi_cli-0.52.dist-info/RECORD +99 -0
  75. kimi_cli-0.52.dist-info/entry_points.txt +3 -0
  76. kimi_cli/agent.py +0 -261
  77. kimi_cli/agents/koder/README.md +0 -3
  78. kimi_cli/prompts/metacmds/__init__.py +0 -4
  79. kimi_cli/soul/wire.py +0 -101
  80. kimi_cli/ui/shell/liveview.py +0 -158
  81. kimi_cli/utils/provider.py +0 -64
  82. kimi_cli-0.35.dist-info/METADATA +0 -24
  83. kimi_cli-0.35.dist-info/RECORD +0 -76
  84. kimi_cli-0.35.dist-info/entry_points.txt +0 -3
  85. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  86. /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
  87. /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
  88. {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
@@ -1,29 +1,33 @@
1
1
  import asyncio
2
2
  import uuid
3
+ from typing import Any
3
4
 
4
- import acp
5
- import streamingjson
6
- from kosong.base.message import (
5
+ import acp # pyright: ignore[reportMissingTypeStubs]
6
+ import streamingjson # pyright: ignore[reportMissingTypeStubs]
7
+ from kosong.chat_provider import ChatProviderError
8
+ from kosong.message import (
7
9
  ContentPart,
8
10
  TextPart,
11
+ ThinkPart,
9
12
  ToolCall,
10
13
  ToolCallPart,
11
14
  )
12
- from kosong.chat_provider import ChatProviderError
13
15
  from kosong.tooling import ToolError, ToolOk, ToolResult
14
16
 
15
- from kimi_cli.soul import LLMNotSet, MaxStepsReached, Soul
16
- from kimi_cli.soul.wire import (
17
+ from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
18
+ from kimi_cli.tools import extract_key_argument
19
+ from kimi_cli.utils.logging import logger
20
+ from kimi_cli.wire import WireUISide
21
+ from kimi_cli.wire.message import (
17
22
  ApprovalRequest,
18
23
  ApprovalResponse,
24
+ CompactionBegin,
25
+ CompactionEnd,
19
26
  StatusUpdate,
20
27
  StepBegin,
21
28
  StepInterrupted,
22
- Wire,
29
+ SubagentEvent,
23
30
  )
24
- from kimi_cli.tools import extract_subtitle
25
- from kimi_cli.ui import RunCancelled, run_soul
26
- from kimi_cli.utils.logging import logger
27
31
 
28
32
 
29
33
  class _ToolCallState:
@@ -50,7 +54,7 @@ class _ToolCallState:
50
54
  def get_title(self) -> str:
51
55
  """Get the current title with subtitle if available."""
52
56
  tool_name = self.tool_call.function.name
53
- subtitle = extract_subtitle(self.lexer, tool_name)
57
+ subtitle = extract_key_argument(self.lexer, tool_name)
54
58
  if subtitle:
55
59
  return f"{tool_name}: {subtitle}"
56
60
  return tool_name
@@ -62,9 +66,10 @@ class _RunState:
62
66
  """Map of tool call ID (LLM-side ID) to tool call state."""
63
67
  self.last_tool_call: _ToolCallState | None = None
64
68
  self.cancel_event = asyncio.Event()
69
+ self.in_thinking = False
65
70
 
66
71
 
67
- class ACPAgentImpl:
72
+ class ACPAgent:
68
73
  """Implementation of the ACP Agent protocol."""
69
74
 
70
75
  def __init__(self, soul: Soul, connection: acp.AgentSideConnection):
@@ -117,12 +122,12 @@ class ACPAgentImpl:
117
122
  logger.warning("Set session mode: {mode}", mode=params.modeId)
118
123
  return None
119
124
 
120
- async def extMethod(self, method: str, params: dict) -> dict:
125
+ async def extMethod(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
121
126
  """Handle extension method."""
122
127
  logger.warning("Unsupported extension method: {method}", method=method)
123
128
  return {}
124
129
 
125
- async def extNotification(self, method: str, params: dict) -> None:
130
+ async def extNotification(self, method: str, params: dict[str, Any]) -> None:
126
131
  """Handle extension notification."""
127
132
  logger.warning("Unsupported extension notification: {method}", method=method)
128
133
 
@@ -172,34 +177,46 @@ class ACPAgentImpl:
172
177
  logger.info("Cancelling running prompt")
173
178
  self.run_state.cancel_event.set()
174
179
 
175
- async def _stream_events(self, wire: Wire):
176
- try:
177
- # expect a StepBegin
178
- assert isinstance(await wire.receive(), StepBegin)
180
+ async def _stream_events(self, wire: WireUISide):
181
+ while True:
182
+ msg = await wire.receive()
179
183
 
180
- while True:
181
- msg = await wire.receive()
184
+ assert self.run_state is not None
185
+ if isinstance(msg, ThinkPart) and not self.run_state.in_thinking:
186
+ await self._send_text("<think>\n")
187
+ self.run_state.in_thinking = True
188
+ if not isinstance(msg, ThinkPart) and self.run_state.in_thinking:
189
+ await self._send_text("\n\n</think>\n\n")
190
+ self.run_state.in_thinking = False
182
191
 
183
- if isinstance(msg, TextPart):
184
- await self._send_text(msg.text)
185
- elif isinstance(msg, ContentPart):
192
+ match msg:
193
+ case StepBegin():
194
+ pass
195
+ case StepInterrupted():
196
+ break
197
+ case CompactionBegin():
198
+ pass
199
+ case CompactionEnd():
200
+ pass
201
+ case StatusUpdate():
202
+ pass
203
+ case ThinkPart(think=think):
204
+ await self._send_text(think)
205
+ case TextPart(text=text):
206
+ await self._send_text(text)
207
+ case ContentPart():
186
208
  logger.warning("Unsupported content part: {part}", part=msg)
187
209
  await self._send_text(f"[{msg.__class__.__name__}]")
188
- elif isinstance(msg, ToolCall):
210
+ case ToolCall():
189
211
  await self._send_tool_call(msg)
190
- elif isinstance(msg, ToolCallPart):
212
+ case ToolCallPart():
191
213
  await self._send_tool_call_part(msg)
192
- elif isinstance(msg, ToolResult):
214
+ case ToolResult():
193
215
  await self._send_tool_result(msg)
194
- elif isinstance(msg, ApprovalRequest):
195
- await self._handle_approval_request(msg)
196
- elif isinstance(msg, StatusUpdate):
197
- # TODO: stream status if needed
216
+ case SubagentEvent():
198
217
  pass
199
- elif isinstance(msg, StepInterrupted):
200
- break
201
- except asyncio.QueueShutDown:
202
- logger.debug("Event stream loop shutting down")
218
+ case ApprovalRequest():
219
+ await self._handle_approval_request(msg)
203
220
 
204
221
  async def _send_text(self, text: str):
205
222
  """Send text chunk to client."""
@@ -322,7 +339,7 @@ class ACPAgentImpl:
322
339
  # Create permission request with options
323
340
  permission_request = acp.RequestPermissionRequest(
324
341
  sessionId=self.session_id,
325
- toolCall=acp.schema.ToolCallUpdate(
342
+ toolCall=acp.schema.ToolCall(
326
343
  toolCallId=state.acp_tool_call_id,
327
344
  content=[
328
345
  acp.schema.ContentToolCallContent(
@@ -388,7 +405,13 @@ def _tool_result_to_acp_content(
388
405
  | acp.schema.FileEditToolCallContent
389
406
  | acp.schema.TerminalToolCallContent
390
407
  ]:
391
- def _to_acp_content(part: ContentPart) -> acp.schema.ContentToolCallContent:
408
+ def _to_acp_content(
409
+ part: ContentPart,
410
+ ) -> (
411
+ acp.schema.ContentToolCallContent
412
+ | acp.schema.FileEditToolCallContent
413
+ | acp.schema.TerminalToolCallContent
414
+ ):
392
415
  if isinstance(part, TextPart):
393
416
  return acp.schema.ContentToolCallContent(
394
417
  type="content", content=acp.schema.TextContentBlock(type="text", text=part.text)
@@ -402,7 +425,13 @@ def _tool_result_to_acp_content(
402
425
  ),
403
426
  )
404
427
 
405
- content = []
428
+ content: list[
429
+ (
430
+ acp.schema.ContentToolCallContent
431
+ | acp.schema.FileEditToolCallContent
432
+ | acp.schema.TerminalToolCallContent
433
+ )
434
+ ] = []
406
435
  if isinstance(tool_result.output, str):
407
436
  content.append(_to_acp_content(TextPart(text=tool_result.output)))
408
437
  elif isinstance(tool_result.output, ContentPart):
@@ -428,7 +457,7 @@ class ACPServer:
428
457
 
429
458
  # Create connection - the library handles all JSON-RPC details!
430
459
  _ = acp.AgentSideConnection(
431
- lambda conn: ACPAgentImpl(self.soul, conn),
460
+ lambda conn: ACPAgent(self.soul, conn),
432
461
  writer,
433
462
  reader,
434
463
  )
@@ -1,23 +1,21 @@
1
1
  import asyncio
2
2
  import json
3
- import signal
4
3
  import sys
5
4
  from functools import partial
6
- from typing import Literal
5
+ from pathlib import Path
7
6
 
8
7
  import aiofiles
9
- from kosong.base.message import Message
10
8
  from kosong.chat_provider import ChatProviderError
9
+ from kosong.message import Message
10
+ from rich import print
11
11
 
12
- from kimi_cli.soul import LLMNotSet, MaxStepsReached
13
- from kimi_cli.soul.kimisoul import KimiSoul
14
- from kimi_cli.soul.wire import StepInterrupted, Wire
15
- from kimi_cli.ui import RunCancelled, run_soul
12
+ from kimi_cli.cli import InputFormat, OutputFormat
13
+ from kimi_cli.soul import LLMNotSet, MaxStepsReached, RunCancelled, Soul, run_soul
16
14
  from kimi_cli.utils.logging import logger
17
15
  from kimi_cli.utils.message import message_extract_text
18
-
19
- InputFormat = Literal["text", "stream-json"]
20
- OutputFormat = Literal["text", "stream-json"]
16
+ from kimi_cli.utils.signals import install_sigint_handler
17
+ from kimi_cli.wire import WireUISide
18
+ from kimi_cli.wire.message import StepInterrupted
21
19
 
22
20
 
23
21
  class PrintApp:
@@ -25,17 +23,23 @@ class PrintApp:
25
23
  An app implementation that prints the agent behavior to the console.
26
24
 
27
25
  Args:
28
- soul (KimiSoul): The soul to run. Only `KimiSoul` is supported.
26
+ soul (Soul): The soul to run.
29
27
  input_format (InputFormat): The input format to use.
30
28
  output_format (OutputFormat): The output format to use.
29
+ context_file (Path): The file to store the context.
31
30
  """
32
31
 
33
- def __init__(self, soul: KimiSoul, input_format: InputFormat, output_format: OutputFormat):
32
+ def __init__(
33
+ self,
34
+ soul: Soul,
35
+ input_format: InputFormat,
36
+ output_format: OutputFormat,
37
+ context_file: Path,
38
+ ):
34
39
  self.soul = soul
35
40
  self.input_format = input_format
36
41
  self.output_format = output_format
37
- self.soul._approval.set_yolo(True)
38
- # TODO(approval): proper approval request handling
42
+ self.context_file = context_file
39
43
 
40
44
  async def run(self, command: str | None = None) -> bool:
41
45
  cancel_event = asyncio.Event()
@@ -45,7 +49,7 @@ class PrintApp:
45
49
  cancel_event.set()
46
50
 
47
51
  loop = asyncio.get_running_loop()
48
- loop.add_signal_handler(signal.SIGINT, _handler)
52
+ remove_sigint = install_sigint_handler(loop, _handler)
49
53
 
50
54
  if command is None and not sys.stdin.isatty() and self.input_format == "text":
51
55
  command = sys.stdin.read().strip()
@@ -92,33 +96,9 @@ class PrintApp:
92
96
  print(f"Unknown error: {e}")
93
97
  raise
94
98
  finally:
95
- loop.remove_signal_handler(signal.SIGINT)
99
+ remove_sigint()
96
100
  return False
97
101
 
98
- # TODO: unify with `_soul_run` in `ShellApp` and `ACPAgentImpl`
99
- async def _soul_run(self, user_input: str):
100
- wire = Wire()
101
- logger.debug("Starting visualization loop")
102
-
103
- if self.output_format == "text":
104
- vis_task = asyncio.create_task(self._visualize_text(wire))
105
- else:
106
- assert self.output_format == "stream-json"
107
- if not self.soul._context._file_backend.exists():
108
- self.soul._context._file_backend.touch()
109
- start_position = self.soul._context._file_backend.stat().st_size
110
- vis_task = asyncio.create_task(self._visualize_stream_json(wire, start_position))
111
-
112
- try:
113
- await self.soul.run(user_input, wire)
114
- finally:
115
- wire.shutdown()
116
- # shutting down the event queue should break the visualization loop
117
- try:
118
- await asyncio.wait_for(vis_task, timeout=0.5)
119
- except TimeoutError:
120
- logger.warning("Visualization loop timed out")
121
-
122
102
  def _read_next_command(self) -> str | None:
123
103
  while True:
124
104
  json_line = sys.stdin.readline()
@@ -144,33 +124,30 @@ class PrintApp:
144
124
  except Exception:
145
125
  logger.warning("Ignoring invalid user message: {json_line}", json_line=json_line)
146
126
 
147
- async def _visualize_text(self, wire: Wire):
148
- try:
127
+ async def _visualize_text(self, wire: WireUISide):
128
+ while True:
129
+ msg = await wire.receive()
130
+ print(msg)
131
+ if isinstance(msg, StepInterrupted):
132
+ break
133
+
134
+ async def _visualize_stream_json(self, wire: WireUISide, start_position: int):
135
+ # TODO: be aware of context compaction
136
+ # FIXME: this is only a temporary impl, may miss the last lines of the context file
137
+ if not self.context_file.exists():
138
+ self.context_file.touch()
139
+ async with aiofiles.open(self.context_file, encoding="utf-8") as f:
140
+ await f.seek(start_position)
149
141
  while True:
150
- msg = await wire.receive()
151
- print(msg)
152
- if isinstance(msg, StepInterrupted):
153
- break
154
- except asyncio.QueueShutDown:
155
- logger.debug("Visualization loop shutting down")
156
-
157
- async def _visualize_stream_json(self, wire: Wire, start_position: int):
158
- try:
159
- async with aiofiles.open(self.soul._context._file_backend, encoding="utf-8") as f:
160
- await f.seek(start_position)
161
- while True:
162
- should_end = False
163
- while wire._queue.qsize() > 0:
164
- msg = wire._queue.get_nowait()
165
- if isinstance(msg, StepInterrupted):
166
- should_end = True
167
-
168
- line = await f.readline()
169
- if not line:
170
- if should_end:
171
- break
172
- await asyncio.sleep(0.1)
173
- continue
174
- print(line, end="")
175
- except asyncio.QueueShutDown:
176
- logger.debug("Visualization loop shutting down")
142
+ should_end = False
143
+ while (msg := wire.receive_nowait()) is not None:
144
+ if isinstance(msg, StepInterrupted):
145
+ should_end = True
146
+
147
+ line = await f.readline()
148
+ if not line:
149
+ if should_end:
150
+ break
151
+ await asyncio.sleep(0.1)
152
+ continue
153
+ print(line, end="")