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/cli.py DELETED
@@ -1,250 +0,0 @@
1
- import asyncio
2
- import json
3
- import sys
4
- from collections.abc import Callable
5
- from pathlib import Path
6
- from typing import Any, Literal, get_args
7
-
8
- import click
9
-
10
- from kimi_cli.constant import VERSION
11
-
12
-
13
- class Reload(Exception):
14
- """Reload configuration."""
15
-
16
- pass
17
-
18
-
19
- UIMode = Literal["shell", "print", "acp"]
20
- InputFormat = Literal["text", "stream-json"]
21
- OutputFormat = Literal["text", "stream-json"]
22
-
23
-
24
- @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
25
- @click.version_option(VERSION)
26
- @click.option(
27
- "--verbose",
28
- is_flag=True,
29
- default=False,
30
- help="Print verbose information. Default: no.",
31
- )
32
- @click.option(
33
- "--debug",
34
- is_flag=True,
35
- default=False,
36
- help="Log debug information. Default: no.",
37
- )
38
- @click.option(
39
- "--agent-file",
40
- type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
41
- default=None,
42
- help="Custom agent specification file. Default: builtin default agent.",
43
- )
44
- @click.option(
45
- "--model",
46
- "-m",
47
- "model_name",
48
- type=str,
49
- default=None,
50
- help="LLM model to use. Default: default model set in config file.",
51
- )
52
- @click.option(
53
- "--work-dir",
54
- "-w",
55
- type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
56
- default=Path.cwd(),
57
- help="Working directory for the agent. Default: current directory.",
58
- )
59
- @click.option(
60
- "--continue",
61
- "-C",
62
- "continue_",
63
- is_flag=True,
64
- default=False,
65
- help="Continue the previous session for the working directory. Default: no.",
66
- )
67
- @click.option(
68
- "--command",
69
- "-c",
70
- "--query",
71
- "-q",
72
- "command",
73
- type=str,
74
- default=None,
75
- help="User query to the agent. Default: prompt interactively.",
76
- )
77
- @click.option(
78
- "--ui",
79
- "ui",
80
- type=click.Choice(get_args(UIMode)),
81
- default="shell",
82
- help="UI mode to use. Default: shell.",
83
- )
84
- @click.option(
85
- "--print",
86
- "ui",
87
- flag_value="print",
88
- help="Run in print mode. Shortcut for `--ui print`. Note: print mode implicitly adds `--yolo`.",
89
- )
90
- @click.option(
91
- "--acp",
92
- "ui",
93
- flag_value="acp",
94
- help="Start ACP server. Shortcut for `--ui acp`.",
95
- )
96
- @click.option(
97
- "--input-format",
98
- type=click.Choice(get_args(InputFormat)),
99
- default=None,
100
- help=(
101
- "Input format to use. Must be used with `--print` "
102
- "and the input must be piped in via stdin. "
103
- "Default: text."
104
- ),
105
- )
106
- @click.option(
107
- "--output-format",
108
- type=click.Choice(get_args(OutputFormat)),
109
- default=None,
110
- help="Output format to use. Must be used with `--print`. Default: text.",
111
- )
112
- @click.option(
113
- "--mcp-config-file",
114
- type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
115
- multiple=True,
116
- help=(
117
- "MCP config file to load. Add this option multiple times to specify multiple MCP configs. "
118
- "Default: none."
119
- ),
120
- )
121
- @click.option(
122
- "--mcp-config",
123
- type=str,
124
- multiple=True,
125
- help=(
126
- "MCP config JSON to load. Add this option multiple times to specify multiple MCP configs. "
127
- "Default: none."
128
- ),
129
- )
130
- @click.option(
131
- "--yolo",
132
- "--yes",
133
- "-y",
134
- "--auto-approve",
135
- "yolo",
136
- is_flag=True,
137
- default=False,
138
- help="Automatically approve all actions. Default: no.",
139
- )
140
- def kimi(
141
- verbose: bool,
142
- debug: bool,
143
- agent_file: Path | None,
144
- model_name: str | None,
145
- work_dir: Path,
146
- continue_: bool,
147
- command: str | None,
148
- ui: UIMode,
149
- input_format: InputFormat | None,
150
- output_format: OutputFormat | None,
151
- mcp_config_file: list[Path],
152
- mcp_config: list[str],
153
- yolo: bool,
154
- ):
155
- """Kimi, your next CLI agent."""
156
- from kimi_cli.app import KimiCLI
157
- from kimi_cli.session import Session
158
- from kimi_cli.share import get_share_dir
159
- from kimi_cli.utils.logging import logger
160
-
161
- def _noop_echo(*args: Any, **kwargs: Any):
162
- pass
163
-
164
- echo: Callable[..., None] = click.echo if verbose else _noop_echo
165
-
166
- logger.add(
167
- get_share_dir() / "logs" / "kimi.log",
168
- level="DEBUG" if debug else "INFO",
169
- rotation="06:00",
170
- retention="10 days",
171
- )
172
-
173
- work_dir = work_dir.absolute()
174
- if continue_:
175
- session = Session.continue_(work_dir)
176
- if session is None:
177
- raise click.BadOptionUsage(
178
- "--continue", "No previous session found for the working directory"
179
- )
180
- echo(f"✓ Continuing previous session: {session.id}")
181
- else:
182
- session = Session.create(work_dir)
183
- echo(f"✓ Created new session: {session.id}")
184
- echo(f"✓ Session history file: {session.history_file}")
185
-
186
- if command is not None:
187
- command = command.strip()
188
- if not command:
189
- raise click.BadOptionUsage("--command", "Command cannot be empty")
190
-
191
- if input_format is not None and ui != "print":
192
- raise click.BadOptionUsage(
193
- "--input-format",
194
- "Input format is only supported for print UI",
195
- )
196
- if output_format is not None and ui != "print":
197
- raise click.BadOptionUsage(
198
- "--output-format",
199
- "Output format is only supported for print UI",
200
- )
201
-
202
- try:
203
- mcp_configs = [json.loads(conf.read_text(encoding="utf-8")) for conf in mcp_config_file]
204
- except json.JSONDecodeError as e:
205
- raise click.BadOptionUsage("--mcp-config-file", f"Invalid JSON: {e}") from e
206
-
207
- try:
208
- mcp_configs += [json.loads(conf) for conf in mcp_config]
209
- except json.JSONDecodeError as e:
210
- raise click.BadOptionUsage("--mcp-config", f"Invalid JSON: {e}") from e
211
-
212
- async def _run() -> bool:
213
- instance = await KimiCLI.create(
214
- session,
215
- yolo=yolo or (ui == "print"), # print mode implies yolo
216
- stream=ui != "print", # use non-streaming mode only for print UI
217
- mcp_configs=mcp_configs,
218
- model_name=model_name,
219
- agent_file=agent_file,
220
- )
221
- match ui:
222
- case "shell":
223
- return await instance.run_shell_mode(command)
224
- case "print":
225
- return await instance.run_print_mode(
226
- input_format or "text",
227
- output_format or "text",
228
- command,
229
- )
230
- case "acp":
231
- if command is not None:
232
- logger.warning("ACP server ignores command argument")
233
- return await instance.run_acp_server()
234
-
235
- while True:
236
- try:
237
- succeeded = asyncio.run(_run())
238
- if not succeeded:
239
- sys.exit(1)
240
- break
241
- except Reload:
242
- continue
243
-
244
-
245
- def main():
246
- kimi()
247
-
248
-
249
- if __name__ == "__main__":
250
- main()
kimi_cli/soul/runtime.py DELETED
@@ -1,96 +0,0 @@
1
- import asyncio
2
- import subprocess
3
- import sys
4
- from datetime import datetime
5
- from pathlib import Path
6
- from typing import NamedTuple
7
-
8
- from kimi_cli.config import Config
9
- from kimi_cli.llm import LLM
10
- from kimi_cli.session import Session
11
- from kimi_cli.soul.approval import Approval
12
- from kimi_cli.soul.denwarenji import DenwaRenji
13
- from kimi_cli.utils.logging import logger
14
-
15
-
16
- class BuiltinSystemPromptArgs(NamedTuple):
17
- """Builtin system prompt arguments."""
18
-
19
- KIMI_NOW: str
20
- """The current datetime."""
21
- KIMI_WORK_DIR: Path
22
- """The current working directory."""
23
- KIMI_WORK_DIR_LS: str
24
- """The directory listing of current working directory."""
25
- KIMI_AGENTS_MD: str # TODO: move to first message from system prompt
26
- """The content of AGENTS.md."""
27
-
28
-
29
- def load_agents_md(work_dir: Path) -> str | None:
30
- paths = [
31
- work_dir / "AGENTS.md",
32
- work_dir / "agents.md",
33
- ]
34
- for path in paths:
35
- if path.is_file():
36
- logger.info("Loaded agents.md: {path}", path=path)
37
- return path.read_text(encoding="utf-8").strip()
38
- logger.info("No AGENTS.md found in {work_dir}", work_dir=work_dir)
39
- return None
40
-
41
-
42
- def _list_work_dir(work_dir: Path) -> str:
43
- if sys.platform == "win32":
44
- ls = subprocess.run(
45
- ["cmd", "/c", "dir", work_dir],
46
- capture_output=True,
47
- text=True,
48
- encoding="utf-8",
49
- errors="replace",
50
- )
51
- else:
52
- ls = subprocess.run(
53
- ["ls", "-la", work_dir],
54
- capture_output=True,
55
- text=True,
56
- encoding="utf-8",
57
- errors="replace",
58
- )
59
- return ls.stdout.strip()
60
-
61
-
62
- class Runtime(NamedTuple):
63
- """Agent runtime."""
64
-
65
- config: Config
66
- llm: LLM | None
67
- session: Session
68
- builtin_args: BuiltinSystemPromptArgs
69
- denwa_renji: DenwaRenji
70
- approval: Approval
71
-
72
- @staticmethod
73
- async def create(
74
- config: Config,
75
- llm: LLM | None,
76
- session: Session,
77
- yolo: bool,
78
- ) -> "Runtime":
79
- ls_output, agents_md = await asyncio.gather(
80
- asyncio.to_thread(_list_work_dir, session.work_dir),
81
- asyncio.to_thread(load_agents_md, session.work_dir),
82
- )
83
-
84
- return Runtime(
85
- config=config,
86
- llm=llm,
87
- session=session,
88
- builtin_args=BuiltinSystemPromptArgs(
89
- KIMI_NOW=datetime.now().astimezone().isoformat(),
90
- KIMI_WORK_DIR=session.work_dir,
91
- KIMI_WORK_DIR_LS=ls_output,
92
- KIMI_AGENTS_MD=agents_md or "",
93
- ),
94
- denwa_renji=DenwaRenji(),
95
- approval=Approval(yolo=yolo),
96
- )
@@ -1,99 +0,0 @@
1
- import asyncio
2
- from pathlib import Path
3
- from typing import override
4
-
5
- from kosong.tooling import CallableTool2, ToolReturnType
6
- from pydantic import BaseModel, Field
7
-
8
- from kimi_cli.soul.approval import Approval
9
- from kimi_cli.tools.utils import ToolRejectedError, ToolResultBuilder, load_desc
10
-
11
- MAX_TIMEOUT = 5 * 60
12
-
13
-
14
- class Params(BaseModel):
15
- command: str = Field(description="The bash command to execute.")
16
- timeout: int = Field(
17
- description=(
18
- "The timeout in seconds for the command to execute. "
19
- "If the command takes longer than this, it will be killed."
20
- ),
21
- default=60,
22
- ge=1,
23
- le=MAX_TIMEOUT,
24
- )
25
-
26
-
27
- class Bash(CallableTool2[Params]):
28
- name: str = "Bash"
29
- description: str = load_desc(Path(__file__).parent / "bash.md", {})
30
- params: type[Params] = Params
31
-
32
- def __init__(self, approval: Approval, **kwargs):
33
- super().__init__(**kwargs)
34
- self._approval = approval
35
-
36
- @override
37
- async def __call__(self, params: Params) -> ToolReturnType:
38
- builder = ToolResultBuilder()
39
-
40
- if not await self._approval.request(
41
- self.name,
42
- "run shell command",
43
- f"Run command `{params.command}`",
44
- ):
45
- return ToolRejectedError()
46
-
47
- def stdout_cb(line: bytes):
48
- line_str = line.decode(errors="replace")
49
- builder.write(line_str)
50
-
51
- def stderr_cb(line: bytes):
52
- line_str = line.decode(errors="replace")
53
- builder.write(line_str)
54
-
55
- try:
56
- exitcode = await _stream_subprocess(
57
- params.command, stdout_cb, stderr_cb, params.timeout
58
- )
59
-
60
- if exitcode == 0:
61
- return builder.ok("Command executed successfully.")
62
- else:
63
- return builder.error(
64
- f"Command failed with exit code: {exitcode}.",
65
- brief=f"Failed with exit code: {exitcode}",
66
- )
67
- except TimeoutError:
68
- return builder.error(
69
- f"Command killed by timeout ({params.timeout}s)",
70
- brief=f"Killed by timeout ({params.timeout}s)",
71
- )
72
-
73
-
74
- async def _stream_subprocess(command: str, stdout_cb, stderr_cb, timeout: int) -> int:
75
- async def _read_stream(stream, cb):
76
- while True:
77
- line = await stream.readline()
78
- if line:
79
- cb(line)
80
- else:
81
- break
82
-
83
- # FIXME: if the event loop is cancelled, an exception may be raised when the process finishes
84
- process = await asyncio.create_subprocess_shell(
85
- command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
86
- )
87
-
88
- try:
89
- await asyncio.wait_for(
90
- asyncio.gather(
91
- _read_stream(process.stdout, stdout_cb),
92
- _read_stream(process.stderr, stderr_cb),
93
- ),
94
- timeout,
95
- )
96
- return await process.wait()
97
- except TimeoutError:
98
- process.kill()
99
- raise
@@ -1,8 +0,0 @@
1
- Apply a unified diff patch to a file.
2
-
3
- **Tips:**
4
- - The patch must be in unified diff format, the format used by `diff -u` and `git diff`.
5
- - Only use this tool on text files.
6
- - The tool will fail with error returned if the patch doesn't apply cleanly.
7
- - The file must exist before applying the patch.
8
- - You should prefer this tool over WriteFile tool and Bash `sed` command when editing an existing file.
@@ -1,143 +0,0 @@
1
- from pathlib import Path
2
- from typing import override
3
-
4
- import aiofiles
5
- import patch_ng
6
- from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
7
- from pydantic import BaseModel, Field
8
-
9
- from kimi_cli.soul.approval import Approval
10
- from kimi_cli.soul.runtime import BuiltinSystemPromptArgs
11
- from kimi_cli.tools.file import FileActions
12
- from kimi_cli.tools.utils import ToolRejectedError
13
-
14
-
15
- class Params(BaseModel):
16
- path: str = Field(description="The absolute path to the file to apply the patch to.")
17
- diff: str = Field(description="The diff content in unified format to apply.")
18
-
19
-
20
- class PatchFile(CallableTool2[Params]):
21
- name: str = "PatchFile"
22
- description: str = (Path(__file__).parent / "patch.md").read_text(encoding="utf-8")
23
- params: type[Params] = Params
24
-
25
- def __init__(self, builtin_args: BuiltinSystemPromptArgs, approval: Approval, **kwargs):
26
- super().__init__(**kwargs)
27
- self._work_dir = builtin_args.KIMI_WORK_DIR
28
- self._approval = approval
29
-
30
- def _validate_path(self, path: Path) -> ToolError | None:
31
- """Validate that the path is safe to patch."""
32
- # Check for path traversal attempts
33
- resolved_path = path.resolve()
34
- resolved_work_dir = Path(self._work_dir).resolve()
35
-
36
- # Ensure the path is within work directory
37
- if not str(resolved_path).startswith(str(resolved_work_dir)):
38
- return ToolError(
39
- message=(
40
- f"`{path}` is outside the working directory. "
41
- "You can only patch files within the working directory."
42
- ),
43
- brief="Path outside working directory",
44
- )
45
- return None
46
-
47
- @override
48
- async def __call__(self, params: Params) -> ToolReturnType:
49
- try:
50
- p = Path(params.path)
51
-
52
- if not p.is_absolute():
53
- return ToolError(
54
- message=(
55
- f"`{params.path}` is not an absolute path. "
56
- "You must provide an absolute path to patch a file."
57
- ),
58
- brief="Invalid path",
59
- )
60
-
61
- # Validate path safety
62
- path_error = self._validate_path(p)
63
- if path_error:
64
- return path_error
65
-
66
- if not p.exists():
67
- return ToolError(
68
- message=f"`{params.path}` does not exist.",
69
- brief="File not found",
70
- )
71
- if not p.is_file():
72
- return ToolError(
73
- message=f"`{params.path}` is not a file.",
74
- brief="Invalid path",
75
- )
76
-
77
- # Request approval
78
- if not await self._approval.request(
79
- self.name,
80
- FileActions.EDIT,
81
- f"Patch file `{params.path}`",
82
- ):
83
- return ToolRejectedError()
84
-
85
- # Read the file content
86
- async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
87
- original_content = await f.read()
88
-
89
- # Create patch object directly from string (no temporary file needed!)
90
- patch_set = patch_ng.fromstring(params.diff.encode("utf-8"))
91
-
92
- # Handle case where patch_ng.fromstring returns False on parse errors
93
- if not patch_set or patch_set is True:
94
- return ToolError(
95
- message=(
96
- "Failed to parse diff content: invalid patch format or no valid hunks found"
97
- ),
98
- brief="Invalid diff format",
99
- )
100
-
101
- # Count total hunks across all items
102
- total_hunks = sum(len(item.hunks) for item in patch_set.items)
103
-
104
- if total_hunks == 0:
105
- return ToolError(
106
- message="No valid hunks found in the diff content",
107
- brief="No hunks found",
108
- )
109
-
110
- # Apply the patch
111
- success = patch_set.apply(root=str(p.parent))
112
-
113
- if not success:
114
- return ToolError(
115
- message=(
116
- "Failed to apply patch - patch may not be compatible with the file content"
117
- ),
118
- brief="Patch application failed",
119
- )
120
-
121
- # Read the modified content to check if changes were made
122
- async with aiofiles.open(p, encoding="utf-8", errors="replace") as f:
123
- modified_content = await f.read()
124
-
125
- # Check if any changes were made
126
- if modified_content == original_content:
127
- return ToolError(
128
- message="No changes were made. The patch does not apply to the file.",
129
- brief="No changes made",
130
- )
131
-
132
- return ToolOk(
133
- output="",
134
- message=(
135
- f"File successfully patched. Applied {total_hunks} hunk(s) to {params.path}."
136
- ),
137
- )
138
-
139
- except Exception as e:
140
- return ToolError(
141
- message=f"Failed to patch file. Error: {e}",
142
- brief="Failed to patch file",
143
- )
kimi_cli/tools/mcp.py DELETED
@@ -1,85 +0,0 @@
1
- import fastmcp
2
- import mcp
3
- from fastmcp.client.client import CallToolResult
4
- from kosong.base.message import AudioURLPart, ContentPart, ImageURLPart, TextPart
5
- from kosong.tooling import CallableTool, ToolOk, ToolReturnType
6
-
7
-
8
- class MCPTool(CallableTool):
9
- def __init__(self, mcp_tool: mcp.Tool, client: fastmcp.Client, **kwargs):
10
- super().__init__(
11
- name=mcp_tool.name,
12
- description=mcp_tool.description or "",
13
- parameters=mcp_tool.inputSchema,
14
- **kwargs,
15
- )
16
- self._mcp_tool = mcp_tool
17
- self._client = client
18
-
19
- async def __call__(self, *args, **kwargs) -> ToolReturnType:
20
- async with self._client as client:
21
- result = await client.call_tool(self._mcp_tool.name, kwargs, timeout=20)
22
- return convert_tool_result(result)
23
-
24
-
25
- def convert_tool_result(result: CallToolResult) -> ToolReturnType:
26
- content: list[ContentPart] = []
27
- for part in result.content:
28
- match part:
29
- case mcp.types.TextContent(text=text):
30
- content.append(TextPart(text=text))
31
- case mcp.types.ImageContent(data=data, mimeType=mimeType):
32
- content.append(
33
- ImageURLPart(
34
- image_url=ImageURLPart.ImageURL(url=f"data:{mimeType};base64,{data}")
35
- )
36
- )
37
- case mcp.types.AudioContent(data=data, mimeType=mimeType):
38
- content.append(
39
- AudioURLPart(
40
- audio_url=AudioURLPart.AudioURL(url=f"data:{mimeType};base64,{data}")
41
- )
42
- )
43
- case mcp.types.EmbeddedResource(
44
- resource=mcp.types.BlobResourceContents(uri=_uri, mimeType=mimeType, blob=blob)
45
- ):
46
- mimeType = mimeType or "application/octet-stream"
47
- if mimeType.startswith("image/"):
48
- content.append(
49
- ImageURLPart(
50
- type="image_url",
51
- image_url=ImageURLPart.ImageURL(
52
- url=f"data:{mimeType};base64,{blob}",
53
- ),
54
- )
55
- )
56
- elif mimeType.startswith("audio/"):
57
- content.append(
58
- AudioURLPart(
59
- type="audio_url",
60
- audio_url=AudioURLPart.AudioURL(url=f"data:{mimeType};base64,{blob}"),
61
- )
62
- )
63
- else:
64
- raise ValueError(f"Unsupported mime type: {mimeType}")
65
- case mcp.types.ResourceLink(uri=uri, mimeType=mimeType, description=_description):
66
- mimeType = mimeType or "application/octet-stream"
67
- if mimeType.startswith("image/"):
68
- content.append(
69
- ImageURLPart(
70
- type="image_url",
71
- image_url=ImageURLPart.ImageURL(url=str(uri)),
72
- )
73
- )
74
- elif mimeType.startswith("audio/"):
75
- content.append(
76
- AudioURLPart(
77
- type="audio_url",
78
- audio_url=AudioURLPart.AudioURL(url=str(uri)),
79
- )
80
- )
81
- else:
82
- raise ValueError(f"Unsupported mime type: {mimeType}")
83
- case _:
84
- raise ValueError(f"Unsupported MCP tool result part: {part}")
85
- return ToolOk(output=content)