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.
- kimi_cli/CHANGELOG.md +349 -40
- kimi_cli/__init__.py +6 -0
- kimi_cli/acp/AGENTS.md +91 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +111 -0
- kimi_cli/acp/kaos.py +270 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +335 -0
- kimi_cli/acp/session.py +445 -0
- kimi_cli/acp/tools.py +158 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/agents/default/agent.yaml +4 -4
- kimi_cli/agents/default/sub.yaml +2 -1
- kimi_cli/agents/default/system.md +79 -21
- kimi_cli/agents/okabe/agent.yaml +17 -0
- kimi_cli/agentspec.py +53 -25
- kimi_cli/app.py +180 -52
- kimi_cli/cli/__init__.py +595 -0
- kimi_cli/cli/__main__.py +8 -0
- kimi_cli/cli/info.py +63 -0
- kimi_cli/cli/mcp.py +349 -0
- kimi_cli/config.py +153 -17
- kimi_cli/constant.py +3 -0
- kimi_cli/exception.py +23 -2
- kimi_cli/flow/__init__.py +117 -0
- kimi_cli/flow/d2.py +376 -0
- kimi_cli/flow/mermaid.py +218 -0
- kimi_cli/llm.py +129 -23
- kimi_cli/metadata.py +32 -7
- kimi_cli/platforms.py +262 -0
- kimi_cli/prompts/__init__.py +2 -0
- kimi_cli/prompts/compact.md +4 -5
- kimi_cli/session.py +223 -31
- kimi_cli/share.py +2 -0
- kimi_cli/skill.py +145 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +351 -0
- kimi_cli/soul/__init__.py +51 -20
- kimi_cli/soul/agent.py +213 -85
- kimi_cli/soul/approval.py +86 -17
- kimi_cli/soul/compaction.py +64 -53
- kimi_cli/soul/context.py +38 -5
- kimi_cli/soul/denwarenji.py +2 -0
- kimi_cli/soul/kimisoul.py +442 -60
- kimi_cli/soul/message.py +54 -54
- kimi_cli/soul/slash.py +72 -0
- kimi_cli/soul/toolset.py +387 -6
- kimi_cli/toad.py +74 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +42 -34
- kimi_cli/tools/display.py +25 -0
- kimi_cli/tools/dmail/__init__.py +10 -10
- kimi_cli/tools/dmail/dmail.md +11 -9
- kimi_cli/tools/file/__init__.py +1 -3
- kimi_cli/tools/file/glob.py +20 -23
- kimi_cli/tools/file/grep.md +1 -1
- kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
- kimi_cli/tools/file/read.md +24 -6
- kimi_cli/tools/file/read.py +134 -50
- kimi_cli/tools/file/replace.md +1 -1
- kimi_cli/tools/file/replace.py +36 -29
- kimi_cli/tools/file/utils.py +282 -0
- kimi_cli/tools/file/write.py +43 -22
- kimi_cli/tools/multiagent/__init__.py +7 -0
- kimi_cli/tools/multiagent/create.md +11 -0
- kimi_cli/tools/multiagent/create.py +50 -0
- kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
- kimi_cli/tools/shell/__init__.py +120 -0
- kimi_cli/tools/{bash → shell}/bash.md +1 -2
- kimi_cli/tools/shell/powershell.md +25 -0
- kimi_cli/tools/test.py +4 -4
- kimi_cli/tools/think/__init__.py +2 -2
- kimi_cli/tools/todo/__init__.py +14 -8
- kimi_cli/tools/utils.py +64 -24
- kimi_cli/tools/web/fetch.py +68 -13
- kimi_cli/tools/web/search.py +10 -12
- kimi_cli/ui/acp/__init__.py +65 -412
- kimi_cli/ui/print/__init__.py +37 -49
- kimi_cli/ui/print/visualize.py +179 -0
- kimi_cli/ui/shell/__init__.py +141 -84
- kimi_cli/ui/shell/console.py +2 -0
- kimi_cli/ui/shell/debug.py +28 -23
- kimi_cli/ui/shell/keyboard.py +5 -1
- kimi_cli/ui/shell/prompt.py +220 -194
- kimi_cli/ui/shell/replay.py +111 -46
- kimi_cli/ui/shell/setup.py +89 -82
- kimi_cli/ui/shell/slash.py +422 -0
- kimi_cli/ui/shell/update.py +4 -2
- kimi_cli/ui/shell/usage.py +271 -0
- kimi_cli/ui/shell/visualize.py +574 -72
- kimi_cli/ui/wire/__init__.py +267 -0
- kimi_cli/ui/wire/jsonrpc.py +142 -0
- kimi_cli/ui/wire/protocol.py +1 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +2 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +12 -7
- kimi_cli/utils/clipboard.py +12 -0
- kimi_cli/utils/datetime.py +37 -0
- kimi_cli/utils/environment.py +58 -0
- kimi_cli/utils/envvar.py +12 -0
- kimi_cli/utils/frontmatter.py +44 -0
- kimi_cli/utils/logging.py +7 -6
- kimi_cli/utils/message.py +9 -14
- kimi_cli/utils/path.py +99 -9
- kimi_cli/utils/pyinstaller.py +6 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/markdown.py +961 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +2 -0
- kimi_cli/utils/slashcmd.py +124 -0
- kimi_cli/utils/string.py +2 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/wire/__init__.py +98 -29
- kimi_cli/wire/serde.py +45 -0
- kimi_cli/wire/types.py +299 -0
- kimi_cli-0.78.dist-info/METADATA +200 -0
- kimi_cli-0.78.dist-info/RECORD +135 -0
- kimi_cli-0.78.dist-info/entry_points.txt +4 -0
- kimi_cli/cli.py +0 -250
- kimi_cli/soul/runtime.py +0 -96
- kimi_cli/tools/bash/__init__.py +0 -99
- kimi_cli/tools/file/patch.md +0 -8
- kimi_cli/tools/file/patch.py +0 -143
- kimi_cli/tools/mcp.py +0 -85
- kimi_cli/ui/shell/liveview.py +0 -386
- kimi_cli/ui/shell/metacmd.py +0 -262
- kimi_cli/wire/message.py +0 -91
- kimi_cli-0.44.dist-info/METADATA +0 -188
- kimi_cli-0.44.dist-info/RECORD +0 -89
- kimi_cli-0.44.dist-info/entry_points.txt +0 -3
- /kimi_cli/tools/{task → multiagent}/task.md +0 -0
- {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
|
-
)
|
kimi_cli/tools/bash/__init__.py
DELETED
|
@@ -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
|
kimi_cli/tools/file/patch.md
DELETED
|
@@ -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.
|
kimi_cli/tools/file/patch.py
DELETED
|
@@ -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)
|