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.

Files changed (137) hide show
  1. kimi_cli/CHANGELOG.md +349 -40
  2. kimi_cli/__init__.py +6 -0
  3. kimi_cli/acp/AGENTS.md +91 -0
  4. kimi_cli/acp/__init__.py +13 -0
  5. kimi_cli/acp/convert.py +111 -0
  6. kimi_cli/acp/kaos.py +270 -0
  7. kimi_cli/acp/mcp.py +46 -0
  8. kimi_cli/acp/server.py +335 -0
  9. kimi_cli/acp/session.py +445 -0
  10. kimi_cli/acp/tools.py +158 -0
  11. kimi_cli/acp/types.py +13 -0
  12. kimi_cli/agents/default/agent.yaml +4 -4
  13. kimi_cli/agents/default/sub.yaml +2 -1
  14. kimi_cli/agents/default/system.md +79 -21
  15. kimi_cli/agents/okabe/agent.yaml +17 -0
  16. kimi_cli/agentspec.py +53 -25
  17. kimi_cli/app.py +180 -52
  18. kimi_cli/cli/__init__.py +595 -0
  19. kimi_cli/cli/__main__.py +8 -0
  20. kimi_cli/cli/info.py +63 -0
  21. kimi_cli/cli/mcp.py +349 -0
  22. kimi_cli/config.py +153 -17
  23. kimi_cli/constant.py +3 -0
  24. kimi_cli/exception.py +23 -2
  25. kimi_cli/flow/__init__.py +117 -0
  26. kimi_cli/flow/d2.py +376 -0
  27. kimi_cli/flow/mermaid.py +218 -0
  28. kimi_cli/llm.py +129 -23
  29. kimi_cli/metadata.py +32 -7
  30. kimi_cli/platforms.py +262 -0
  31. kimi_cli/prompts/__init__.py +2 -0
  32. kimi_cli/prompts/compact.md +4 -5
  33. kimi_cli/session.py +223 -31
  34. kimi_cli/share.py +2 -0
  35. kimi_cli/skill.py +145 -0
  36. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  37. kimi_cli/skills/skill-creator/SKILL.md +351 -0
  38. kimi_cli/soul/__init__.py +51 -20
  39. kimi_cli/soul/agent.py +213 -85
  40. kimi_cli/soul/approval.py +86 -17
  41. kimi_cli/soul/compaction.py +64 -53
  42. kimi_cli/soul/context.py +38 -5
  43. kimi_cli/soul/denwarenji.py +2 -0
  44. kimi_cli/soul/kimisoul.py +442 -60
  45. kimi_cli/soul/message.py +54 -54
  46. kimi_cli/soul/slash.py +72 -0
  47. kimi_cli/soul/toolset.py +387 -6
  48. kimi_cli/toad.py +74 -0
  49. kimi_cli/tools/AGENTS.md +5 -0
  50. kimi_cli/tools/__init__.py +42 -34
  51. kimi_cli/tools/display.py +25 -0
  52. kimi_cli/tools/dmail/__init__.py +10 -10
  53. kimi_cli/tools/dmail/dmail.md +11 -9
  54. kimi_cli/tools/file/__init__.py +1 -3
  55. kimi_cli/tools/file/glob.py +20 -23
  56. kimi_cli/tools/file/grep.md +1 -1
  57. kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
  58. kimi_cli/tools/file/read.md +24 -6
  59. kimi_cli/tools/file/read.py +134 -50
  60. kimi_cli/tools/file/replace.md +1 -1
  61. kimi_cli/tools/file/replace.py +36 -29
  62. kimi_cli/tools/file/utils.py +282 -0
  63. kimi_cli/tools/file/write.py +43 -22
  64. kimi_cli/tools/multiagent/__init__.py +7 -0
  65. kimi_cli/tools/multiagent/create.md +11 -0
  66. kimi_cli/tools/multiagent/create.py +50 -0
  67. kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
  68. kimi_cli/tools/shell/__init__.py +120 -0
  69. kimi_cli/tools/{bash → shell}/bash.md +1 -2
  70. kimi_cli/tools/shell/powershell.md +25 -0
  71. kimi_cli/tools/test.py +4 -4
  72. kimi_cli/tools/think/__init__.py +2 -2
  73. kimi_cli/tools/todo/__init__.py +14 -8
  74. kimi_cli/tools/utils.py +64 -24
  75. kimi_cli/tools/web/fetch.py +68 -13
  76. kimi_cli/tools/web/search.py +10 -12
  77. kimi_cli/ui/acp/__init__.py +65 -412
  78. kimi_cli/ui/print/__init__.py +37 -49
  79. kimi_cli/ui/print/visualize.py +179 -0
  80. kimi_cli/ui/shell/__init__.py +141 -84
  81. kimi_cli/ui/shell/console.py +2 -0
  82. kimi_cli/ui/shell/debug.py +28 -23
  83. kimi_cli/ui/shell/keyboard.py +5 -1
  84. kimi_cli/ui/shell/prompt.py +220 -194
  85. kimi_cli/ui/shell/replay.py +111 -46
  86. kimi_cli/ui/shell/setup.py +89 -82
  87. kimi_cli/ui/shell/slash.py +422 -0
  88. kimi_cli/ui/shell/update.py +4 -2
  89. kimi_cli/ui/shell/usage.py +271 -0
  90. kimi_cli/ui/shell/visualize.py +574 -72
  91. kimi_cli/ui/wire/__init__.py +267 -0
  92. kimi_cli/ui/wire/jsonrpc.py +142 -0
  93. kimi_cli/ui/wire/protocol.py +1 -0
  94. kimi_cli/utils/__init__.py +0 -0
  95. kimi_cli/utils/aiohttp.py +2 -0
  96. kimi_cli/utils/aioqueue.py +72 -0
  97. kimi_cli/utils/broadcast.py +37 -0
  98. kimi_cli/utils/changelog.py +12 -7
  99. kimi_cli/utils/clipboard.py +12 -0
  100. kimi_cli/utils/datetime.py +37 -0
  101. kimi_cli/utils/environment.py +58 -0
  102. kimi_cli/utils/envvar.py +12 -0
  103. kimi_cli/utils/frontmatter.py +44 -0
  104. kimi_cli/utils/logging.py +7 -6
  105. kimi_cli/utils/message.py +9 -14
  106. kimi_cli/utils/path.py +99 -9
  107. kimi_cli/utils/pyinstaller.py +6 -0
  108. kimi_cli/utils/rich/__init__.py +33 -0
  109. kimi_cli/utils/rich/columns.py +99 -0
  110. kimi_cli/utils/rich/markdown.py +961 -0
  111. kimi_cli/utils/rich/markdown_sample.md +108 -0
  112. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  113. kimi_cli/utils/signals.py +2 -0
  114. kimi_cli/utils/slashcmd.py +124 -0
  115. kimi_cli/utils/string.py +2 -0
  116. kimi_cli/utils/term.py +168 -0
  117. kimi_cli/utils/typing.py +20 -0
  118. kimi_cli/wire/__init__.py +98 -29
  119. kimi_cli/wire/serde.py +45 -0
  120. kimi_cli/wire/types.py +299 -0
  121. kimi_cli-0.78.dist-info/METADATA +200 -0
  122. kimi_cli-0.78.dist-info/RECORD +135 -0
  123. kimi_cli-0.78.dist-info/entry_points.txt +4 -0
  124. kimi_cli/cli.py +0 -250
  125. kimi_cli/soul/runtime.py +0 -96
  126. kimi_cli/tools/bash/__init__.py +0 -99
  127. kimi_cli/tools/file/patch.md +0 -8
  128. kimi_cli/tools/file/patch.py +0 -143
  129. kimi_cli/tools/mcp.py +0 -85
  130. kimi_cli/ui/shell/liveview.py +0 -386
  131. kimi_cli/ui/shell/metacmd.py +0 -262
  132. kimi_cli/wire/message.py +0 -91
  133. kimi_cli-0.44.dist-info/METADATA +0 -188
  134. kimi_cli-0.44.dist-info/RECORD +0 -89
  135. kimi_cli-0.44.dist-info/entry_points.txt +0 -3
  136. /kimi_cli/tools/{task → multiagent}/task.md +0 -0
  137. {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
kimi_cli/tools/utils.py CHANGED
@@ -2,17 +2,34 @@ import re
2
2
  import string
3
3
  from pathlib import Path
4
4
 
5
- from kosong.tooling import ToolError, ToolOk
5
+ from jinja2 import Environment
6
+ from kosong.tooling import BriefDisplayBlock, DisplayBlock, ToolError, ToolReturnValue
7
+ from kosong.utils.typing import JsonType
6
8
 
7
9
 
8
10
  def load_desc(path: Path, substitutions: dict[str, str] | None = None) -> str:
9
11
  """Load a tool description from a file, with optional substitutions."""
10
12
  description = path.read_text(encoding="utf-8")
11
13
  if substitutions:
12
- description = string.Template(description).substitute(substitutions)
14
+ description = string.Template(description).safe_substitute(substitutions)
13
15
  return description
14
16
 
15
17
 
18
+ def load_desc_jinja(path: Path, context: dict[str, object] | None = None) -> str:
19
+ """Load a tool description from a file, rendered via Jinja2."""
20
+ description = path.read_text(encoding="utf-8")
21
+ env = Environment(
22
+ autoescape=False,
23
+ keep_trailing_newline=True,
24
+ lstrip_blocks=True,
25
+ trim_blocks=True,
26
+ variable_start_string="${",
27
+ variable_end_string="}",
28
+ )
29
+ template = env.from_string(description)
30
+ return template.render(context or {})
31
+
32
+
16
33
  def truncate_line(line: str, max_length: int, marker: str = "...") -> str:
17
34
  """
18
35
  Truncate a line if it exceeds `max_length`, preserving the beginning and the line break.
@@ -53,6 +70,23 @@ class ToolResultBuilder:
53
70
  self._n_chars = 0
54
71
  self._n_lines = 0
55
72
  self._truncation_happened = False
73
+ self._display: list[DisplayBlock] = []
74
+ self._extras: dict[str, JsonType] | None = None
75
+
76
+ @property
77
+ def is_full(self) -> bool:
78
+ """Check if output buffer is full due to character limit."""
79
+ return self._n_chars >= self.max_chars
80
+
81
+ @property
82
+ def n_chars(self) -> int:
83
+ """Get current character count."""
84
+ return self._n_chars
85
+
86
+ @property
87
+ def n_lines(self) -> int:
88
+ """Get current line count."""
89
+ return self._n_lines
56
90
 
57
91
  def write(self, text: str) -> int:
58
92
  """
@@ -93,8 +127,18 @@ class ToolResultBuilder:
93
127
 
94
128
  return chars_written
95
129
 
96
- def ok(self, message: str = "", *, brief: str = "") -> ToolOk:
97
- """Create a ToolOk result with the current output."""
130
+ def display(self, *blocks: DisplayBlock) -> None:
131
+ """Add display blocks to the tool result."""
132
+ self._display.extend(blocks)
133
+
134
+ def extras(self, **extras: JsonType) -> None:
135
+ """Add extra data to the tool result."""
136
+ if self._extras is None:
137
+ self._extras = {}
138
+ self._extras.update(extras)
139
+
140
+ def ok(self, message: str = "", *, brief: str = "") -> ToolReturnValue:
141
+ """Create a ToolReturnValue with is_error=False and the current output."""
98
142
  output = "".join(self._buffer)
99
143
 
100
144
  final_message = message
@@ -106,11 +150,16 @@ class ToolResultBuilder:
106
150
  final_message += f" {truncation_msg}"
107
151
  else:
108
152
  final_message = truncation_msg
153
+ return ToolReturnValue(
154
+ is_error=False,
155
+ output=output,
156
+ message=final_message,
157
+ display=([BriefDisplayBlock(text=brief)] if brief else []) + self._display,
158
+ extras=self._extras,
159
+ )
109
160
 
110
- return ToolOk(output=output, message=final_message, brief=brief)
111
-
112
- def error(self, message: str, *, brief: str) -> ToolError:
113
- """Create a ToolError result with the current output."""
161
+ def error(self, message: str, *, brief: str) -> ToolReturnValue:
162
+ """Create a ToolReturnValue with is_error=True and the current output."""
114
163
  output = "".join(self._buffer)
115
164
 
116
165
  final_message = message
@@ -121,22 +170,13 @@ class ToolResultBuilder:
121
170
  else:
122
171
  final_message = truncation_msg
123
172
 
124
- return ToolError(output=output, message=final_message, brief=brief)
125
-
126
- @property
127
- def is_full(self) -> bool:
128
- """Check if output buffer is full due to character limit."""
129
- return self._n_chars >= self.max_chars
130
-
131
- @property
132
- def n_chars(self) -> int:
133
- """Get current character count."""
134
- return self._n_chars
135
-
136
- @property
137
- def n_lines(self) -> int:
138
- """Get current line count."""
139
- return self._n_lines
173
+ return ToolReturnValue(
174
+ is_error=True,
175
+ output=output,
176
+ message=final_message,
177
+ display=([BriefDisplayBlock(text=brief)] if brief else []) + self._display,
178
+ extras=self._extras,
179
+ )
140
180
 
141
181
 
142
182
  class ToolRejectedError(ToolError):
@@ -3,11 +3,15 @@ from typing import override
3
3
 
4
4
  import aiohttp
5
5
  import trafilatura
6
- from kosong.tooling import CallableTool2, ToolReturnType
6
+ from kosong.tooling import CallableTool2, ToolReturnValue
7
7
  from pydantic import BaseModel, Field
8
8
 
9
+ from kimi_cli.config import Config
10
+ from kimi_cli.constant import USER_AGENT
11
+ from kimi_cli.soul.toolset import get_current_tool_call_or_none
9
12
  from kimi_cli.tools.utils import ToolResultBuilder, load_desc
10
13
  from kimi_cli.utils.aiohttp import new_client_session
14
+ from kimi_cli.utils.logging import logger
11
15
 
12
16
 
13
17
  class Params(BaseModel):
@@ -19,10 +23,23 @@ class FetchURL(CallableTool2[Params]):
19
23
  description: str = load_desc(Path(__file__).parent / "fetch.md", {})
20
24
  params: type[Params] = Params
21
25
 
26
+ def __init__(self, config: Config):
27
+ super().__init__()
28
+ self._service_config = config.services.moonshot_fetch
29
+
22
30
  @override
23
- async def __call__(self, params: Params) -> ToolReturnType:
31
+ async def __call__(self, params: Params) -> ToolReturnValue:
32
+ if self._service_config:
33
+ ret = await self._fetch_with_service(params)
34
+ if not ret.is_error:
35
+ return ret
36
+ logger.warning("Failed to fetch URL via service: {error}", error=ret.message)
37
+ # fallback to local fetch if service fetch fails
38
+ return await self.fetch_with_http_get(params)
39
+
40
+ @staticmethod
41
+ async def fetch_with_http_get(params: Params) -> ToolReturnValue:
24
42
  builder = ToolResultBuilder(max_line_length=None)
25
-
26
43
  try:
27
44
  async with (
28
45
  new_client_session() as session,
@@ -45,7 +62,12 @@ class FetchURL(CallableTool2[Params]):
45
62
  brief=f"HTTP {response.status} error",
46
63
  )
47
64
 
48
- html = await response.text()
65
+ resp_text = await response.text()
66
+
67
+ content_type = response.headers.get(aiohttp.hdrs.CONTENT_TYPE, "").lower()
68
+ if content_type.startswith(("text/plain", "text/markdown")):
69
+ builder.write(resp_text)
70
+ return builder.ok("The returned content is the full content of the page.")
49
71
  except aiohttp.ClientError as e:
50
72
  return builder.error(
51
73
  (
@@ -55,14 +77,14 @@ class FetchURL(CallableTool2[Params]):
55
77
  brief="Network error",
56
78
  )
57
79
 
58
- if not html:
80
+ if not resp_text:
59
81
  return builder.ok(
60
82
  "The response body is empty.",
61
83
  brief="Empty response body",
62
84
  )
63
85
 
64
86
  extracted_text = trafilatura.extract(
65
- html,
87
+ resp_text,
66
88
  include_comments=True,
67
89
  include_tables=True,
68
90
  include_formatting=False,
@@ -83,13 +105,46 @@ class FetchURL(CallableTool2[Params]):
83
105
  builder.write(extracted_text)
84
106
  return builder.ok("The returned content is the main text content extracted from the page.")
85
107
 
108
+ async def _fetch_with_service(self, params: Params) -> ToolReturnValue:
109
+ assert self._service_config is not None
86
110
 
87
- if __name__ == "__main__":
88
- import asyncio
111
+ tool_call = get_current_tool_call_or_none()
112
+ assert tool_call is not None, "Tool call is expected to be set"
89
113
 
90
- async def main():
91
- fetch_url_tool = FetchURL()
92
- result = await fetch_url_tool(Params(url="https://trafilatura.readthedocs.io/en/latest/"))
93
- print(result)
114
+ builder = ToolResultBuilder(max_line_length=None)
115
+ headers = {
116
+ "User-Agent": USER_AGENT,
117
+ "Authorization": f"Bearer {self._service_config.api_key.get_secret_value()}",
118
+ "Accept": "text/markdown",
119
+ "X-Msh-Tool-Call-Id": tool_call.id,
120
+ **(self._service_config.custom_headers or {}),
121
+ }
94
122
 
95
- asyncio.run(main())
123
+ try:
124
+ async with (
125
+ new_client_session() as session,
126
+ session.post(
127
+ self._service_config.base_url,
128
+ headers=headers,
129
+ json={"url": params.url},
130
+ ) as response,
131
+ ):
132
+ if response.status != 200:
133
+ return builder.error(
134
+ f"Failed to fetch URL via service. Status: {response.status}.",
135
+ brief="Failed to fetch URL via fetch service",
136
+ )
137
+
138
+ content = await response.text()
139
+ builder.write(content)
140
+ return builder.ok(
141
+ "The returned content is the main content extracted from the page."
142
+ )
143
+ except aiohttp.ClientError as e:
144
+ return builder.error(
145
+ (
146
+ f"Failed to fetch URL via service due to network error: {str(e)}. "
147
+ "This may indicate the service is unreachable."
148
+ ),
149
+ brief="Network error when calling fetch service",
150
+ )
@@ -1,12 +1,13 @@
1
1
  from pathlib import Path
2
2
  from typing import override
3
3
 
4
- from kosong.tooling import CallableTool2, ToolReturnType
4
+ from kosong.tooling import CallableTool2, ToolReturnValue
5
5
  from pydantic import BaseModel, Field, ValidationError
6
6
 
7
7
  from kimi_cli.config import Config
8
8
  from kimi_cli.constant import USER_AGENT
9
9
  from kimi_cli.soul.toolset import get_current_tool_call_or_none
10
+ from kimi_cli.tools import SkipThisTool
10
11
  from kimi_cli.tools.utils import ToolResultBuilder, load_desc
11
12
  from kimi_cli.utils.aiohttp import new_client_session
12
13
 
@@ -39,19 +40,16 @@ class SearchWeb(CallableTool2[Params]):
39
40
  description: str = load_desc(Path(__file__).parent / "search.md", {})
40
41
  params: type[Params] = Params
41
42
 
42
- def __init__(self, config: Config, **kwargs):
43
- super().__init__(**kwargs)
44
- if config.services.moonshot_search is not None:
45
- self._base_url = config.services.moonshot_search.base_url
46
- self._api_key = config.services.moonshot_search.api_key.get_secret_value()
47
- self._custom_headers = config.services.moonshot_search.custom_headers or {}
48
- else:
49
- self._base_url = ""
50
- self._api_key = ""
51
- self._custom_headers = {}
43
+ def __init__(self, config: Config):
44
+ super().__init__()
45
+ if config.services.moonshot_search is None:
46
+ raise SkipThisTool()
47
+ self._base_url = config.services.moonshot_search.base_url
48
+ self._api_key = config.services.moonshot_search.api_key.get_secret_value()
49
+ self._custom_headers = config.services.moonshot_search.custom_headers or {}
52
50
 
53
51
  @override
54
- async def __call__(self, params: Params) -> ToolReturnType:
52
+ async def __call__(self, params: Params) -> ToolReturnValue:
55
53
  builder = ToolResultBuilder(max_line_length=None)
56
54
 
57
55
  if not self._base_url or not self._api_key: