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
@@ -0,0 +1,108 @@
1
+ # Markdown Sample Document
2
+
3
+ This is a comprehensive sample document showcasing various Markdown elements.
4
+
5
+ ## Level 2 Heading
6
+
7
+ ### Level 3 Heading
8
+
9
+ Here's some regular text with **bold text**, *italic text*, and `inline code`.
10
+
11
+ ## Lists
12
+
13
+ ### Unordered List
14
+
15
+ - First item
16
+ - Second item
17
+ - Nested item 1
18
+ - Nested item 2
19
+ - Third item
20
+
21
+ ### Ordered List
22
+
23
+ 1. First step
24
+ 2. Second step
25
+ 1. Sub-step A
26
+ 2. Sub-step B
27
+ 3. Third step
28
+
29
+ ### Mixed List
30
+
31
+ 1. First item
32
+ - Sub-item with bullet
33
+ - Another sub-item
34
+ 2. Second item
35
+ 1. Numbered sub-item
36
+ 2. Another numbered sub-item
37
+
38
+ ## Links and References
39
+
40
+ Here's a [link to GitHub](https://github.com) and another [relative link](../README.md).
41
+
42
+ ## Code Blocks
43
+
44
+ ```python
45
+ def hello_world():
46
+ """A simple function to demonstrate code blocks."""
47
+ print("Hello, World!")
48
+ return 42
49
+
50
+ # Call the function
51
+ result = hello_world()
52
+ ```
53
+
54
+ ```bash
55
+ # Bash example
56
+ echo "This is a bash script"
57
+ ls -la /tmp
58
+ ```
59
+
60
+ ## Blockquotes
61
+
62
+ > This is a blockquote.
63
+ > It can span multiple lines.
64
+ >
65
+ > > And it can be nested too!
66
+
67
+ ## Tables
68
+
69
+ | Column 1 | Column 2 | Column 3 |
70
+ |----------|----------|----------|
71
+ | Cell 1 | Cell 2 | Cell 3 |
72
+ | Left | Center | Right |
73
+ | Foo | Bar | Baz |
74
+
75
+ ## Horizontal Rules
76
+
77
+ ---
78
+
79
+ Here's some text after a horizontal rule.
80
+
81
+ ---
82
+
83
+ ## Inline Formatting
84
+
85
+ You can combine **bold and *italic*** text, or use `code` within paragraphs.
86
+
87
+ **Important**: Always test your `code` snippets before deployment.
88
+
89
+ ## Advanced Features
90
+
91
+ ### Task Lists
92
+
93
+ - [x] Completed task
94
+ - [ ] Pending task
95
+ - [ ] Another pending task
96
+
97
+ ### Definition Lists
98
+
99
+ Term 1
100
+ : Definition of term 1
101
+
102
+ Term 2
103
+ : Definition of term 2
104
+ : Another definition for term 2
105
+
106
+ ---
107
+
108
+ *This document demonstrates comprehensive Markdown formatting capabilities.*
@@ -0,0 +1,2 @@
1
+ - First
2
+ - Second
@@ -0,0 +1,41 @@
1
+ import asyncio
2
+ import contextlib
3
+ import signal
4
+ from collections.abc import Callable
5
+
6
+
7
+ def install_sigint_handler(
8
+ loop: asyncio.AbstractEventLoop, handler: Callable[[], None]
9
+ ) -> Callable[[], None]:
10
+ """
11
+ Install a SIGINT handler that works on Unix and Windows.
12
+
13
+ On Unix event loops, prefer `loop.add_signal_handler`.
14
+ On Windows (or other platforms) where it is not implemented, fall back to
15
+ `signal.signal`. The fallback cannot be removed from the loop, but we
16
+ restore the previous handler on uninstall.
17
+
18
+ Returns:
19
+ A function that removes the installed handler. It is guaranteed that
20
+ no exceptions are raised when calling the returned function.
21
+ """
22
+
23
+ try:
24
+ loop.add_signal_handler(signal.SIGINT, handler)
25
+
26
+ def remove() -> None:
27
+ with contextlib.suppress(RuntimeError):
28
+ loop.remove_signal_handler(signal.SIGINT)
29
+
30
+ return remove
31
+ except RuntimeError:
32
+ # Windows ProactorEventLoop and some environments do not support
33
+ # add_signal_handler. Use synchronous signal handling as a fallback.
34
+ previous = signal.getsignal(signal.SIGINT)
35
+ signal.signal(signal.SIGINT, lambda signum, frame: handler())
36
+
37
+ def remove() -> None:
38
+ with contextlib.suppress(RuntimeError):
39
+ signal.signal(signal.SIGINT, previous)
40
+
41
+ return remove
kimi_cli/utils/string.py CHANGED
@@ -1,4 +1,6 @@
1
+ import random
1
2
  import re
3
+ import string
2
4
 
3
5
  _NEWLINE_RE = re.compile(r"[\r\n]+")
4
6
 
@@ -10,3 +12,9 @@ def shorten_middle(text: str, width: int, remove_newline: bool = True) -> str:
10
12
  if remove_newline:
11
13
  text = _NEWLINE_RE.sub(" ", text)
12
14
  return text[: width // 2] + "..." + text[-width // 2 :]
15
+
16
+
17
+ def random_string(length: int = 8) -> str:
18
+ """Generate a random string of fixed length."""
19
+ letters = string.ascii_lowercase
20
+ return "".join(random.choice(letters) for _ in range(length))
kimi_cli/utils/term.py ADDED
@@ -0,0 +1,114 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import time
5
+
6
+
7
+ def ensure_new_line() -> None:
8
+ """Ensure the next prompt starts at column 0 regardless of prior command output."""
9
+
10
+ if not sys.stdout.isatty() or not sys.stdin.isatty():
11
+ return
12
+
13
+ needs_break = True
14
+ if sys.platform == "win32":
15
+ column = _cursor_column_windows()
16
+ needs_break = column not in (None, 0)
17
+ else:
18
+ column = _cursor_column_unix()
19
+ needs_break = column not in (None, 1)
20
+
21
+ if needs_break:
22
+ _write_newline()
23
+
24
+
25
+ def _cursor_column_unix() -> int | None:
26
+ assert sys.platform != "win32"
27
+
28
+ import select
29
+ import termios
30
+ import tty
31
+
32
+ _CURSOR_QUERY = "\x1b[6n"
33
+ _CURSOR_POSITION_RE = re.compile(r"\x1b\[(\d+);(\d+)R")
34
+
35
+ fd = sys.stdin.fileno()
36
+ oldterm = termios.tcgetattr(fd)
37
+
38
+ try:
39
+ tty.setcbreak(fd)
40
+ sys.stdout.write(_CURSOR_QUERY)
41
+ sys.stdout.flush()
42
+
43
+ response = ""
44
+ deadline = time.monotonic() + 0.2
45
+ while time.monotonic() < deadline:
46
+ timeout = max(0.01, deadline - time.monotonic())
47
+ ready, _, _ = select.select([sys.stdin], [], [], timeout)
48
+ if not ready:
49
+ continue
50
+ try:
51
+ chunk = os.read(fd, 32)
52
+ except OSError:
53
+ break
54
+ if not chunk:
55
+ break
56
+ response += chunk.decode(encoding="utf-8", errors="ignore")
57
+ match = _CURSOR_POSITION_RE.search(response)
58
+ if match:
59
+ return int(match.group(2))
60
+ finally:
61
+ termios.tcsetattr(fd, termios.TCSADRAIN, oldterm)
62
+
63
+ return None
64
+
65
+
66
+ def _cursor_column_windows() -> int | None:
67
+ assert sys.platform == "win32"
68
+
69
+ import ctypes
70
+ from ctypes import wintypes
71
+
72
+ kernel32 = ctypes.windll.kernel32
73
+ _STD_OUTPUT_HANDLE = -11 # Windows API constant for standard output handle
74
+ handle = kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
75
+ invalid_handle_value = ctypes.c_void_p(-1).value
76
+ if handle in (0, invalid_handle_value):
77
+ return None
78
+
79
+ class COORD(ctypes.Structure):
80
+ _fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)]
81
+
82
+ class SMALL_RECT(ctypes.Structure):
83
+ _fields_ = [
84
+ ("Left", wintypes.SHORT),
85
+ ("Top", wintypes.SHORT),
86
+ ("Right", wintypes.SHORT),
87
+ ("Bottom", wintypes.SHORT),
88
+ ]
89
+
90
+ class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
91
+ _fields_ = [
92
+ ("dwSize", COORD),
93
+ ("dwCursorPosition", COORD),
94
+ ("wAttributes", wintypes.WORD),
95
+ ("srWindow", SMALL_RECT),
96
+ ("dwMaximumWindowSize", COORD),
97
+ ]
98
+
99
+ csbi = CONSOLE_SCREEN_BUFFER_INFO()
100
+ if not kernel32.GetConsoleScreenBufferInfo(handle, ctypes.byref(csbi)):
101
+ return None
102
+
103
+ return int(csbi.dwCursorPosition.X)
104
+
105
+
106
+ def _write_newline() -> None:
107
+ sys.stdout.write("\n")
108
+ sys.stdout.flush()
109
+
110
+
111
+ if __name__ == "__main__":
112
+ print("test", end="", flush=True)
113
+ ensure_new_line()
114
+ print("next line")
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+
3
+ from kosong.message import ContentPart, ToolCallPart
4
+
5
+ from kimi_cli.utils.logging import logger
6
+ from kimi_cli.wire.message import WireMessage
7
+
8
+
9
+ class Wire:
10
+ """
11
+ A channel for communication between the soul and the UI during a soul run.
12
+ """
13
+
14
+ def __init__(self):
15
+ self._queue = asyncio.Queue[WireMessage]()
16
+ self._soul_side = WireSoulSide(self._queue)
17
+ self._ui_side = WireUISide(self._queue)
18
+
19
+ @property
20
+ def soul_side(self) -> "WireSoulSide":
21
+ return self._soul_side
22
+
23
+ @property
24
+ def ui_side(self) -> "WireUISide":
25
+ return self._ui_side
26
+
27
+ def shutdown(self) -> None:
28
+ logger.debug("Shutting down wire")
29
+ self._queue.shutdown()
30
+
31
+
32
+ class WireSoulSide:
33
+ """
34
+ The soul side of a wire.
35
+ """
36
+
37
+ def __init__(self, queue: asyncio.Queue[WireMessage]):
38
+ self._queue = queue
39
+
40
+ def send(self, msg: WireMessage) -> None:
41
+ if not isinstance(msg, ContentPart | ToolCallPart):
42
+ logger.debug("Sending wire message: {msg}", msg=msg)
43
+ try:
44
+ self._queue.put_nowait(msg)
45
+ except asyncio.QueueShutDown:
46
+ logger.info("Failed to send wire message, queue is shut down: {msg}", msg=msg)
47
+
48
+
49
+ class WireUISide:
50
+ """
51
+ The UI side of a wire.
52
+ """
53
+
54
+ def __init__(self, queue: asyncio.Queue[WireMessage]):
55
+ self._queue = queue
56
+
57
+ async def receive(self) -> WireMessage:
58
+ msg = await self._queue.get()
59
+ if not isinstance(msg, ContentPart | ToolCallPart):
60
+ logger.debug("Receiving wire message: {msg}", msg=msg)
61
+ return msg
62
+
63
+ def receive_nowait(self) -> WireMessage | None:
64
+ """
65
+ Try receive a message without waiting. If no message is available, return None.
66
+ """
67
+ try:
68
+ msg = self._queue.get_nowait()
69
+ except asyncio.QueueEmpty:
70
+ return None
71
+ if not isinstance(msg, ContentPart | ToolCallPart):
72
+ logger.debug("Receiving wire message: {msg}", msg=msg)
73
+ return msg
@@ -0,0 +1,191 @@
1
+ import asyncio
2
+ import uuid
3
+ from collections.abc import Sequence
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Any, NamedTuple
6
+
7
+ from kosong.message import ContentPart, ToolCall, ToolCallPart
8
+ from kosong.tooling import ToolOk, ToolResult
9
+
10
+ if TYPE_CHECKING:
11
+ from kimi_cli.soul import StatusSnapshot
12
+
13
+
14
+ class StepBegin(NamedTuple):
15
+ n: int
16
+
17
+
18
+ class StepInterrupted:
19
+ pass
20
+
21
+
22
+ class CompactionBegin:
23
+ """
24
+ Indicates that a compaction just began.
25
+ This event must be sent during a step, which means, between `StepBegin` and `StepInterrupted`.
26
+ And, there must be a `CompactionEnd` directly following this event.
27
+ """
28
+
29
+ pass
30
+
31
+
32
+ class CompactionEnd:
33
+ """
34
+ Indicates that a compaction just ended.
35
+ This event must be sent directly after a `CompactionBegin` event.
36
+ """
37
+
38
+ pass
39
+
40
+
41
+ class StatusUpdate(NamedTuple):
42
+ status: "StatusSnapshot"
43
+
44
+
45
+ class SubagentEvent(NamedTuple):
46
+ task_tool_call_id: str
47
+ event: "Event"
48
+
49
+
50
+ type ControlFlowEvent = StepBegin | StepInterrupted | CompactionBegin | CompactionEnd | StatusUpdate
51
+ type Event = ControlFlowEvent | ContentPart | ToolCall | ToolCallPart | ToolResult | SubagentEvent
52
+
53
+
54
+ class ApprovalResponse(Enum):
55
+ APPROVE = "approve"
56
+ APPROVE_FOR_SESSION = "approve_for_session"
57
+ REJECT = "reject"
58
+
59
+
60
+ class ApprovalRequest:
61
+ def __init__(self, tool_call_id: str, sender: str, action: str, description: str):
62
+ self.id = str(uuid.uuid4())
63
+ self.tool_call_id = tool_call_id
64
+ self.sender = sender
65
+ self.action = action
66
+ self.description = description
67
+ self._future = asyncio.Future[ApprovalResponse]()
68
+
69
+ def __repr__(self) -> str:
70
+ return (
71
+ f"ApprovalRequest(id={self.id}, tool_call_id={self.tool_call_id}, "
72
+ f"sender={self.sender}, action={self.action}, description={self.description})"
73
+ )
74
+
75
+ async def wait(self) -> ApprovalResponse:
76
+ """
77
+ Wait for the request to be resolved or cancelled.
78
+
79
+ Returns:
80
+ ApprovalResponse: The response to the approval request.
81
+ """
82
+ return await self._future
83
+
84
+ def resolve(self, response: ApprovalResponse) -> None:
85
+ """
86
+ Resolve the approval request with the given response.
87
+ This will cause the `wait()` method to return the response.
88
+ """
89
+ self._future.set_result(response)
90
+
91
+ @property
92
+ def resolved(self) -> bool:
93
+ """Whether the request is resolved."""
94
+ return self._future.done()
95
+
96
+
97
+ type WireMessage = Event | ApprovalRequest
98
+
99
+
100
+ def serialize_event(event: Event) -> dict[str, Any]:
101
+ """
102
+ Convert an event message into a JSON-serializable dictionary.
103
+ """
104
+ match event:
105
+ case StepBegin():
106
+ return {"type": "step_begin", "payload": {"n": event.n}}
107
+ case StepInterrupted():
108
+ return {"type": "step_interrupted"}
109
+ case CompactionBegin():
110
+ return {"type": "compaction_begin"}
111
+ case CompactionEnd():
112
+ return {"type": "compaction_end"}
113
+ case StatusUpdate():
114
+ return {
115
+ "type": "status_update",
116
+ "payload": {"context_usage": event.status.context_usage},
117
+ }
118
+ case ContentPart():
119
+ return {
120
+ "type": "content_part",
121
+ "payload": event.model_dump(mode="json", exclude_none=True),
122
+ }
123
+ case ToolCall():
124
+ return {
125
+ "type": "tool_call",
126
+ "payload": event.model_dump(mode="json", exclude_none=True),
127
+ }
128
+ case ToolCallPart():
129
+ return {
130
+ "type": "tool_call_part",
131
+ "payload": event.model_dump(mode="json", exclude_none=True),
132
+ }
133
+ case ToolResult():
134
+ return {
135
+ "type": "tool_result",
136
+ "payload": serialize_tool_result(event),
137
+ }
138
+ case SubagentEvent():
139
+ return {
140
+ "type": "subagent_event",
141
+ "payload": {
142
+ "task_tool_call_id": event.task_tool_call_id,
143
+ "event": serialize_event(event.event),
144
+ },
145
+ }
146
+
147
+
148
+ def serialize_approval_request(request: ApprovalRequest) -> dict[str, Any]:
149
+ """
150
+ Convert an ApprovalRequest into a JSON-serializable dictionary.
151
+ """
152
+ return {
153
+ "id": request.id,
154
+ "tool_call_id": request.tool_call_id,
155
+ "sender": request.sender,
156
+ "action": request.action,
157
+ "description": request.description,
158
+ }
159
+
160
+
161
+ def serialize_tool_result(result: ToolResult) -> dict[str, Any]:
162
+ if isinstance(result.result, ToolOk):
163
+ ok = True
164
+ result_data = {
165
+ "output": _serialize_tool_output(result.result.output),
166
+ "message": result.result.message,
167
+ "brief": result.result.brief,
168
+ }
169
+ else:
170
+ ok = False
171
+ result_data = {
172
+ "output": result.result.output,
173
+ "message": result.result.message,
174
+ "brief": result.result.brief,
175
+ }
176
+ return {
177
+ "tool_call_id": result.tool_call_id,
178
+ "ok": ok,
179
+ "result": result_data,
180
+ }
181
+
182
+
183
+ def _serialize_tool_output(
184
+ output: str | ContentPart | Sequence[ContentPart],
185
+ ) -> str | list[Any] | dict[str, Any]:
186
+ if isinstance(output, str):
187
+ return output
188
+ elif isinstance(output, ContentPart):
189
+ return output.model_dump(mode="json", exclude_none=True)
190
+ else: # Sequence[ContentPart]
191
+ return [part.model_dump(mode="json", exclude_none=True) for part in output]