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.
- kimi_cli/CHANGELOG.md +165 -0
- kimi_cli/__init__.py +0 -374
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/app.py +208 -0
- kimi_cli/cli.py +321 -0
- kimi_cli/config.py +33 -16
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +144 -3
- kimi_cli/metadata.py +6 -69
- kimi_cli/prompts/__init__.py +4 -0
- kimi_cli/session.py +103 -0
- kimi_cli/soul/__init__.py +130 -9
- kimi_cli/soul/agent.py +159 -0
- kimi_cli/soul/approval.py +5 -6
- kimi_cli/soul/compaction.py +106 -0
- kimi_cli/soul/context.py +1 -1
- kimi_cli/soul/kimisoul.py +180 -80
- kimi_cli/soul/message.py +6 -6
- kimi_cli/soul/runtime.py +96 -0
- kimi_cli/soul/toolset.py +3 -2
- kimi_cli/tools/__init__.py +35 -31
- kimi_cli/tools/bash/__init__.py +25 -9
- kimi_cli/tools/bash/cmd.md +31 -0
- kimi_cli/tools/dmail/__init__.py +5 -4
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +4 -4
- kimi_cli/tools/file/grep.py +36 -19
- kimi_cli/tools/file/patch.py +52 -10
- kimi_cli/tools/file/read.py +6 -5
- kimi_cli/tools/file/replace.py +16 -4
- kimi_cli/tools/file/write.py +16 -4
- kimi_cli/tools/mcp.py +7 -4
- kimi_cli/tools/task/__init__.py +60 -41
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +4 -2
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +13 -12
- kimi_cli/ui/__init__.py +0 -68
- kimi_cli/ui/acp/__init__.py +67 -38
- kimi_cli/ui/print/__init__.py +46 -69
- kimi_cli/ui/shell/__init__.py +145 -154
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +183 -0
- kimi_cli/ui/shell/metacmd.py +34 -81
- kimi_cli/ui/shell/prompt.py +245 -28
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/setup.py +19 -19
- kimi_cli/ui/shell/update.py +11 -5
- kimi_cli/ui/shell/visualize.py +576 -0
- kimi_cli/ui/wire/README.md +109 -0
- kimi_cli/ui/wire/__init__.py +340 -0
- kimi_cli/ui/wire/jsonrpc.py +48 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +6 -2
- kimi_cli/utils/clipboard.py +10 -0
- kimi_cli/utils/message.py +15 -1
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/markdown.py +959 -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 +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/utils/term.py +114 -0
- kimi_cli/wire/__init__.py +73 -0
- kimi_cli/wire/message.py +191 -0
- kimi_cli-0.52.dist-info/METADATA +186 -0
- kimi_cli-0.52.dist-info/RECORD +99 -0
- kimi_cli-0.52.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/prompts/metacmds/__init__.py +0 -4
- kimi_cli/soul/wire.py +0 -101
- kimi_cli/ui/shell/liveview.py +0 -158
- kimi_cli/utils/provider.py +0 -64
- kimi_cli-0.35.dist-info/METADATA +0 -24
- kimi_cli-0.35.dist-info/RECORD +0 -76
- kimi_cli-0.35.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
- /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
kimi_cli/app.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import SecretStr
|
|
9
|
+
|
|
10
|
+
from kimi_cli.agentspec import DEFAULT_AGENT_FILE
|
|
11
|
+
from kimi_cli.cli import InputFormat, OutputFormat
|
|
12
|
+
from kimi_cli.config import LLMModel, LLMProvider, load_config
|
|
13
|
+
from kimi_cli.llm import augment_provider_with_env_vars, create_llm
|
|
14
|
+
from kimi_cli.session import Session
|
|
15
|
+
from kimi_cli.soul import LLMNotSet, LLMNotSupported
|
|
16
|
+
from kimi_cli.soul.agent import load_agent
|
|
17
|
+
from kimi_cli.soul.context import Context
|
|
18
|
+
from kimi_cli.soul.kimisoul import KimiSoul
|
|
19
|
+
from kimi_cli.soul.runtime import Runtime
|
|
20
|
+
from kimi_cli.utils.logging import StreamToLogger, logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KimiCLI:
|
|
24
|
+
@staticmethod
|
|
25
|
+
async def create(
|
|
26
|
+
session: Session,
|
|
27
|
+
*,
|
|
28
|
+
yolo: bool = False,
|
|
29
|
+
stream: bool = True, # TODO: remove this when we have a correct print mode impl
|
|
30
|
+
mcp_configs: list[dict[str, Any]] | None = None,
|
|
31
|
+
config_file: Path | None = None,
|
|
32
|
+
model_name: str | None = None,
|
|
33
|
+
thinking: bool = False,
|
|
34
|
+
agent_file: Path | None = None,
|
|
35
|
+
) -> "KimiCLI":
|
|
36
|
+
"""
|
|
37
|
+
Create a KimiCLI instance.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
session (Session): A session created by `Session.create` or `Session.continue_`.
|
|
41
|
+
yolo (bool, optional): Approve all actions without confirmation. Defaults to False.
|
|
42
|
+
stream (bool, optional): Use stream mode when calling LLM API. Defaults to True.
|
|
43
|
+
config_file (Path | None, optional): Path to the configuration file. Defaults to None.
|
|
44
|
+
model_name (str | None, optional): Name of the model to use. Defaults to None.
|
|
45
|
+
agent_file (Path | None, optional): Path to the agent file. Defaults to None.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
FileNotFoundError: When the agent file is not found.
|
|
49
|
+
ConfigError(KimiCLIException): When the configuration is invalid.
|
|
50
|
+
AgentSpecError(KimiCLIException): When the agent specification is invalid.
|
|
51
|
+
"""
|
|
52
|
+
config = load_config(config_file)
|
|
53
|
+
logger.info("Loaded config: {config}", config=config)
|
|
54
|
+
|
|
55
|
+
model: LLMModel | None = None
|
|
56
|
+
provider: LLMProvider | None = None
|
|
57
|
+
|
|
58
|
+
# try to use config file
|
|
59
|
+
if not model_name and config.default_model:
|
|
60
|
+
# no --model specified && default model is set in config
|
|
61
|
+
model = config.models[config.default_model]
|
|
62
|
+
provider = config.providers[model.provider]
|
|
63
|
+
if model_name and model_name in config.models:
|
|
64
|
+
# --model specified && model is set in config
|
|
65
|
+
model = config.models[model_name]
|
|
66
|
+
provider = config.providers[model.provider]
|
|
67
|
+
|
|
68
|
+
if not model:
|
|
69
|
+
model = LLMModel(provider="", model="", max_context_size=100_000)
|
|
70
|
+
provider = LLMProvider(type="kimi", base_url="", api_key=SecretStr(""))
|
|
71
|
+
|
|
72
|
+
# try overwrite with environment variables
|
|
73
|
+
assert provider is not None
|
|
74
|
+
assert model is not None
|
|
75
|
+
env_overrides = augment_provider_with_env_vars(provider, model)
|
|
76
|
+
|
|
77
|
+
if not provider.base_url or not model.model:
|
|
78
|
+
llm = None
|
|
79
|
+
else:
|
|
80
|
+
logger.info("Using LLM provider: {provider}", provider=provider)
|
|
81
|
+
logger.info("Using LLM model: {model}", model=model)
|
|
82
|
+
llm = create_llm(provider, model, stream=stream, session_id=session.id)
|
|
83
|
+
|
|
84
|
+
runtime = await Runtime.create(config, llm, session, yolo)
|
|
85
|
+
|
|
86
|
+
if agent_file is None:
|
|
87
|
+
agent_file = DEFAULT_AGENT_FILE
|
|
88
|
+
agent = await load_agent(agent_file, runtime, mcp_configs=mcp_configs or [])
|
|
89
|
+
|
|
90
|
+
context = Context(session.history_file)
|
|
91
|
+
await context.restore()
|
|
92
|
+
|
|
93
|
+
soul = KimiSoul(
|
|
94
|
+
agent,
|
|
95
|
+
runtime,
|
|
96
|
+
context=context,
|
|
97
|
+
)
|
|
98
|
+
try:
|
|
99
|
+
soul.set_thinking(thinking)
|
|
100
|
+
except (LLMNotSet, LLMNotSupported) as e:
|
|
101
|
+
logger.warning("Failed to enable thinking mode: {error}", error=e)
|
|
102
|
+
return KimiCLI(soul, runtime, env_overrides)
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
_soul: KimiSoul,
|
|
107
|
+
_runtime: Runtime,
|
|
108
|
+
_env_overrides: dict[str, str],
|
|
109
|
+
) -> None:
|
|
110
|
+
self._soul = _soul
|
|
111
|
+
self._runtime = _runtime
|
|
112
|
+
self._env_overrides = _env_overrides
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def soul(self) -> KimiSoul:
|
|
116
|
+
"""Get the KimiSoul instance."""
|
|
117
|
+
return self._soul
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def session(self) -> Session:
|
|
121
|
+
"""Get the Session instance."""
|
|
122
|
+
return self._runtime.session
|
|
123
|
+
|
|
124
|
+
@contextlib.contextmanager
|
|
125
|
+
def _app_env(self) -> Generator[None]:
|
|
126
|
+
original_cwd = Path.cwd()
|
|
127
|
+
os.chdir(self._runtime.session.work_dir)
|
|
128
|
+
try:
|
|
129
|
+
# to ignore possible warnings from dateparser
|
|
130
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
131
|
+
with contextlib.redirect_stderr(StreamToLogger()):
|
|
132
|
+
yield
|
|
133
|
+
finally:
|
|
134
|
+
os.chdir(original_cwd)
|
|
135
|
+
|
|
136
|
+
async def run_shell_mode(self, command: str | None = None) -> bool:
|
|
137
|
+
from kimi_cli.ui.shell import ShellApp, WelcomeInfoItem
|
|
138
|
+
|
|
139
|
+
welcome_info = [
|
|
140
|
+
WelcomeInfoItem(name="Directory", value=str(self._runtime.session.work_dir)),
|
|
141
|
+
WelcomeInfoItem(name="Session", value=self._runtime.session.id),
|
|
142
|
+
]
|
|
143
|
+
if base_url := self._env_overrides.get("KIMI_BASE_URL"):
|
|
144
|
+
welcome_info.append(
|
|
145
|
+
WelcomeInfoItem(
|
|
146
|
+
name="API URL",
|
|
147
|
+
value=f"{base_url} (from KIMI_BASE_URL)",
|
|
148
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
if not self._runtime.llm:
|
|
152
|
+
welcome_info.append(
|
|
153
|
+
WelcomeInfoItem(
|
|
154
|
+
name="Model",
|
|
155
|
+
value="not set, send /setup to configure",
|
|
156
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
elif "KIMI_MODEL_NAME" in self._env_overrides:
|
|
160
|
+
welcome_info.append(
|
|
161
|
+
WelcomeInfoItem(
|
|
162
|
+
name="Model",
|
|
163
|
+
value=f"{self._soul.model_name} (from KIMI_MODEL_NAME)",
|
|
164
|
+
level=WelcomeInfoItem.Level.WARN,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
welcome_info.append(
|
|
169
|
+
WelcomeInfoItem(
|
|
170
|
+
name="Model",
|
|
171
|
+
value=self._soul.model_name,
|
|
172
|
+
level=WelcomeInfoItem.Level.INFO,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
with self._app_env():
|
|
176
|
+
app = ShellApp(self._soul, welcome_info=welcome_info)
|
|
177
|
+
return await app.run(command)
|
|
178
|
+
|
|
179
|
+
async def run_print_mode(
|
|
180
|
+
self,
|
|
181
|
+
input_format: InputFormat,
|
|
182
|
+
output_format: OutputFormat,
|
|
183
|
+
command: str | None = None,
|
|
184
|
+
) -> bool:
|
|
185
|
+
from kimi_cli.ui.print import PrintApp
|
|
186
|
+
|
|
187
|
+
with self._app_env():
|
|
188
|
+
app = PrintApp(
|
|
189
|
+
self._soul,
|
|
190
|
+
input_format,
|
|
191
|
+
output_format,
|
|
192
|
+
self._runtime.session.history_file,
|
|
193
|
+
)
|
|
194
|
+
return await app.run(command)
|
|
195
|
+
|
|
196
|
+
async def run_acp_server(self) -> bool:
|
|
197
|
+
from kimi_cli.ui.acp import ACPServer
|
|
198
|
+
|
|
199
|
+
with self._app_env():
|
|
200
|
+
app = ACPServer(self._soul)
|
|
201
|
+
return await app.run()
|
|
202
|
+
|
|
203
|
+
async def run_wire_server(self) -> bool:
|
|
204
|
+
from kimi_cli.ui.wire import WireServer
|
|
205
|
+
|
|
206
|
+
with self._app_env():
|
|
207
|
+
server = WireServer(self._soul)
|
|
208
|
+
return await server.run()
|
kimi_cli/cli.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kimi_cli.constant import VERSION
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Reload(Exception):
|
|
14
|
+
"""Reload configuration."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
cli = typer.Typer(
|
|
20
|
+
add_completion=False,
|
|
21
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
22
|
+
help="Kimi, your next CLI agent.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
UIMode = Literal["shell", "print", "acp", "wire"]
|
|
26
|
+
InputFormat = Literal["text", "stream-json"]
|
|
27
|
+
OutputFormat = Literal["text", "stream-json"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _version_callback(value: bool) -> None:
|
|
31
|
+
if value:
|
|
32
|
+
typer.echo(f"kimi, version {VERSION}")
|
|
33
|
+
raise typer.Exit()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cli.command()
|
|
37
|
+
def kimi(
|
|
38
|
+
version: Annotated[
|
|
39
|
+
bool,
|
|
40
|
+
typer.Option(
|
|
41
|
+
"--version",
|
|
42
|
+
"-V",
|
|
43
|
+
help="Show version and exit.",
|
|
44
|
+
callback=_version_callback,
|
|
45
|
+
is_eager=True,
|
|
46
|
+
),
|
|
47
|
+
] = False,
|
|
48
|
+
verbose: Annotated[
|
|
49
|
+
bool,
|
|
50
|
+
typer.Option(
|
|
51
|
+
"--verbose",
|
|
52
|
+
help="Print verbose information. Default: no.",
|
|
53
|
+
),
|
|
54
|
+
] = False,
|
|
55
|
+
debug: Annotated[
|
|
56
|
+
bool,
|
|
57
|
+
typer.Option(
|
|
58
|
+
"--debug",
|
|
59
|
+
help="Log debug information. Default: no.",
|
|
60
|
+
),
|
|
61
|
+
] = False,
|
|
62
|
+
agent_file: Annotated[
|
|
63
|
+
Path | None,
|
|
64
|
+
typer.Option(
|
|
65
|
+
"--agent-file",
|
|
66
|
+
exists=True,
|
|
67
|
+
file_okay=True,
|
|
68
|
+
dir_okay=False,
|
|
69
|
+
readable=True,
|
|
70
|
+
help="Custom agent specification file. Default: builtin default agent.",
|
|
71
|
+
),
|
|
72
|
+
] = None,
|
|
73
|
+
model_name: Annotated[
|
|
74
|
+
str | None,
|
|
75
|
+
typer.Option(
|
|
76
|
+
"--model",
|
|
77
|
+
"-m",
|
|
78
|
+
help="LLM model to use. Default: default model set in config file.",
|
|
79
|
+
),
|
|
80
|
+
] = None,
|
|
81
|
+
work_dir: Annotated[
|
|
82
|
+
Path | None,
|
|
83
|
+
typer.Option(
|
|
84
|
+
"--work-dir",
|
|
85
|
+
"-w",
|
|
86
|
+
exists=True,
|
|
87
|
+
file_okay=False,
|
|
88
|
+
dir_okay=True,
|
|
89
|
+
readable=True,
|
|
90
|
+
writable=True,
|
|
91
|
+
help="Working directory for the agent. Default: current directory.",
|
|
92
|
+
),
|
|
93
|
+
] = None,
|
|
94
|
+
continue_: Annotated[
|
|
95
|
+
bool,
|
|
96
|
+
typer.Option(
|
|
97
|
+
"--continue",
|
|
98
|
+
"-C",
|
|
99
|
+
help="Continue the previous session for the working directory. Default: no.",
|
|
100
|
+
),
|
|
101
|
+
] = False,
|
|
102
|
+
command: Annotated[
|
|
103
|
+
str | None,
|
|
104
|
+
typer.Option(
|
|
105
|
+
"--command",
|
|
106
|
+
"-c",
|
|
107
|
+
"--query",
|
|
108
|
+
"-q",
|
|
109
|
+
help="User query to the agent. Default: prompt interactively.",
|
|
110
|
+
),
|
|
111
|
+
] = None,
|
|
112
|
+
print_mode: Annotated[
|
|
113
|
+
bool,
|
|
114
|
+
typer.Option(
|
|
115
|
+
"--print",
|
|
116
|
+
help=(
|
|
117
|
+
"Run in print mode (non-interactive). Note: print mode implicitly adds `--yolo`."
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
] = False,
|
|
121
|
+
acp_mode: Annotated[
|
|
122
|
+
bool,
|
|
123
|
+
typer.Option(
|
|
124
|
+
"--acp",
|
|
125
|
+
help="Run as ACP server.",
|
|
126
|
+
),
|
|
127
|
+
] = False,
|
|
128
|
+
wire_mode: Annotated[
|
|
129
|
+
bool,
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--wire",
|
|
132
|
+
help="Run as Wire server (experimental).",
|
|
133
|
+
),
|
|
134
|
+
] = False,
|
|
135
|
+
input_format: Annotated[
|
|
136
|
+
InputFormat | None,
|
|
137
|
+
typer.Option(
|
|
138
|
+
"--input-format",
|
|
139
|
+
help=(
|
|
140
|
+
"Input format to use. Must be used with `--print` "
|
|
141
|
+
"and the input must be piped in via stdin. "
|
|
142
|
+
"Default: text."
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
] = None,
|
|
146
|
+
output_format: Annotated[
|
|
147
|
+
OutputFormat | None,
|
|
148
|
+
typer.Option(
|
|
149
|
+
"--output-format",
|
|
150
|
+
help="Output format to use. Must be used with `--print`. Default: text.",
|
|
151
|
+
),
|
|
152
|
+
] = None,
|
|
153
|
+
mcp_config_file: Annotated[
|
|
154
|
+
list[Path] | None,
|
|
155
|
+
typer.Option(
|
|
156
|
+
"--mcp-config-file",
|
|
157
|
+
exists=True,
|
|
158
|
+
file_okay=True,
|
|
159
|
+
dir_okay=False,
|
|
160
|
+
readable=True,
|
|
161
|
+
help=(
|
|
162
|
+
"MCP config file to load. Add this option multiple times to specify multiple MCP "
|
|
163
|
+
"configs. Default: none."
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
] = None,
|
|
167
|
+
mcp_config: Annotated[
|
|
168
|
+
list[str] | None,
|
|
169
|
+
typer.Option(
|
|
170
|
+
"--mcp-config",
|
|
171
|
+
help=(
|
|
172
|
+
"MCP config JSON to load. Add this option multiple times to specify multiple MCP "
|
|
173
|
+
"configs. Default: none."
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
] = None,
|
|
177
|
+
yolo: Annotated[
|
|
178
|
+
bool,
|
|
179
|
+
typer.Option(
|
|
180
|
+
"--yolo",
|
|
181
|
+
"--yes",
|
|
182
|
+
"-y",
|
|
183
|
+
"--auto-approve",
|
|
184
|
+
help="Automatically approve all actions. Default: no.",
|
|
185
|
+
),
|
|
186
|
+
] = False,
|
|
187
|
+
thinking: Annotated[
|
|
188
|
+
bool,
|
|
189
|
+
typer.Option(
|
|
190
|
+
"--thinking",
|
|
191
|
+
help="Enable thinking mode if supported. Default: no.",
|
|
192
|
+
),
|
|
193
|
+
] = False,
|
|
194
|
+
):
|
|
195
|
+
"""Kimi, your next CLI agent."""
|
|
196
|
+
del version # handled in the callback
|
|
197
|
+
|
|
198
|
+
from kimi_cli.app import KimiCLI
|
|
199
|
+
from kimi_cli.session import Session
|
|
200
|
+
from kimi_cli.share import get_share_dir
|
|
201
|
+
from kimi_cli.utils.logging import logger
|
|
202
|
+
|
|
203
|
+
def _noop_echo(*args: Any, **kwargs: Any):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
special_flags = {
|
|
207
|
+
"--print": print_mode,
|
|
208
|
+
"--acp": acp_mode,
|
|
209
|
+
"--wire": wire_mode,
|
|
210
|
+
}
|
|
211
|
+
active_specials = [flag for flag, active in special_flags.items() if active]
|
|
212
|
+
if len(active_specials) > 1:
|
|
213
|
+
raise typer.BadParameter(
|
|
214
|
+
f"Cannot combine {', '.join(active_specials)}.",
|
|
215
|
+
param_hint=active_specials[0],
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
ui: UIMode = "shell"
|
|
219
|
+
if print_mode:
|
|
220
|
+
ui = "print"
|
|
221
|
+
elif acp_mode:
|
|
222
|
+
ui = "acp"
|
|
223
|
+
elif wire_mode:
|
|
224
|
+
ui = "wire"
|
|
225
|
+
|
|
226
|
+
echo: Callable[..., None] = typer.echo if verbose else _noop_echo
|
|
227
|
+
|
|
228
|
+
if debug:
|
|
229
|
+
logger.enable("kosong")
|
|
230
|
+
logger.add(
|
|
231
|
+
get_share_dir() / "logs" / "kimi.log",
|
|
232
|
+
# FIXME: configure level for different modules
|
|
233
|
+
level="TRACE" if debug else "INFO",
|
|
234
|
+
rotation="06:00",
|
|
235
|
+
retention="10 days",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
work_dir = (work_dir or Path.cwd()).absolute()
|
|
239
|
+
if continue_:
|
|
240
|
+
session = Session.continue_(work_dir)
|
|
241
|
+
if session is None:
|
|
242
|
+
raise typer.BadParameter(
|
|
243
|
+
"No previous session found for the working directory",
|
|
244
|
+
param_hint="--continue",
|
|
245
|
+
)
|
|
246
|
+
echo(f"✓ Continuing previous session: {session.id}")
|
|
247
|
+
else:
|
|
248
|
+
session = Session.create(work_dir)
|
|
249
|
+
echo(f"✓ Created new session: {session.id}")
|
|
250
|
+
echo(f"✓ Session history file: {session.history_file}")
|
|
251
|
+
|
|
252
|
+
if command is not None:
|
|
253
|
+
command = command.strip()
|
|
254
|
+
if not command:
|
|
255
|
+
raise typer.BadParameter("Command cannot be empty", param_hint="--command")
|
|
256
|
+
|
|
257
|
+
if input_format is not None and ui != "print":
|
|
258
|
+
raise typer.BadParameter(
|
|
259
|
+
"Input format is only supported for print UI",
|
|
260
|
+
param_hint="--input-format",
|
|
261
|
+
)
|
|
262
|
+
if output_format is not None and ui != "print":
|
|
263
|
+
raise typer.BadParameter(
|
|
264
|
+
"Output format is only supported for print UI",
|
|
265
|
+
param_hint="--output-format",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
file_configs = list(mcp_config_file or [])
|
|
269
|
+
raw_mcp_config = list(mcp_config or [])
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
mcp_configs = [json.loads(conf.read_text(encoding="utf-8")) for conf in file_configs]
|
|
273
|
+
except json.JSONDecodeError as e:
|
|
274
|
+
raise typer.BadParameter(f"Invalid JSON: {e}", param_hint="--mcp-config-file") from e
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
mcp_configs += [json.loads(conf) for conf in raw_mcp_config]
|
|
278
|
+
except json.JSONDecodeError as e:
|
|
279
|
+
raise typer.BadParameter(f"Invalid JSON: {e}", param_hint="--mcp-config") from e
|
|
280
|
+
|
|
281
|
+
async def _run() -> bool:
|
|
282
|
+
instance = await KimiCLI.create(
|
|
283
|
+
session,
|
|
284
|
+
yolo=yolo or (ui == "print"), # print mode implies yolo
|
|
285
|
+
stream=ui != "print", # use non-streaming mode only for print UI
|
|
286
|
+
mcp_configs=mcp_configs,
|
|
287
|
+
model_name=model_name,
|
|
288
|
+
thinking=thinking,
|
|
289
|
+
agent_file=agent_file,
|
|
290
|
+
)
|
|
291
|
+
match ui:
|
|
292
|
+
case "shell":
|
|
293
|
+
return await instance.run_shell_mode(command)
|
|
294
|
+
case "print":
|
|
295
|
+
return await instance.run_print_mode(
|
|
296
|
+
input_format or "text",
|
|
297
|
+
output_format or "text",
|
|
298
|
+
command,
|
|
299
|
+
)
|
|
300
|
+
case "acp":
|
|
301
|
+
if command is not None:
|
|
302
|
+
logger.warning("ACP server ignores command argument")
|
|
303
|
+
return await instance.run_acp_server()
|
|
304
|
+
case "wire":
|
|
305
|
+
if command is not None:
|
|
306
|
+
logger.warning("Wire server ignores command argument")
|
|
307
|
+
return await instance.run_wire_server()
|
|
308
|
+
|
|
309
|
+
while True:
|
|
310
|
+
try:
|
|
311
|
+
succeeded = asyncio.run(_run())
|
|
312
|
+
if succeeded:
|
|
313
|
+
session.mark_as_last()
|
|
314
|
+
break
|
|
315
|
+
sys.exit(1)
|
|
316
|
+
except Reload:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
cli()
|
kimi_cli/config.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Self
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field, SecretStr, ValidationError, field_serializer, model_validator
|
|
6
6
|
|
|
7
|
+
from kimi_cli.exception import ConfigError
|
|
8
|
+
from kimi_cli.llm import ModelCapability, ProviderType
|
|
7
9
|
from kimi_cli.share import get_share_dir
|
|
8
10
|
from kimi_cli.utils.logging import logger
|
|
9
11
|
|
|
@@ -11,12 +13,14 @@ from kimi_cli.utils.logging import logger
|
|
|
11
13
|
class LLMProvider(BaseModel):
|
|
12
14
|
"""LLM provider configuration."""
|
|
13
15
|
|
|
14
|
-
type:
|
|
16
|
+
type: ProviderType
|
|
15
17
|
"""Provider type"""
|
|
16
18
|
base_url: str
|
|
17
19
|
"""API base URL"""
|
|
18
20
|
api_key: SecretStr
|
|
19
21
|
"""API key"""
|
|
22
|
+
custom_headers: dict[str, str] | None = None
|
|
23
|
+
"""Custom headers to include in API requests"""
|
|
20
24
|
|
|
21
25
|
@field_serializer("api_key", when_used="json")
|
|
22
26
|
def dump_secret(self, v: SecretStr):
|
|
@@ -32,6 +36,8 @@ class LLMModel(BaseModel):
|
|
|
32
36
|
"""Model name"""
|
|
33
37
|
max_context_size: int
|
|
34
38
|
"""Maximum context size (unit: tokens)"""
|
|
39
|
+
capabilities: set[ModelCapability] | None = None
|
|
40
|
+
"""Model capabilities"""
|
|
35
41
|
|
|
36
42
|
|
|
37
43
|
class LoopControl(BaseModel):
|
|
@@ -50,6 +56,8 @@ class MoonshotSearchConfig(BaseModel):
|
|
|
50
56
|
"""Base URL for Moonshot Search service."""
|
|
51
57
|
api_key: SecretStr
|
|
52
58
|
"""API key for Moonshot Search service."""
|
|
59
|
+
custom_headers: dict[str, str] | None = None
|
|
60
|
+
"""Custom headers to include in API requests."""
|
|
53
61
|
|
|
54
62
|
@field_serializer("api_key", when_used="json")
|
|
55
63
|
def dump_secret(self, v: SecretStr):
|
|
@@ -99,13 +107,21 @@ def get_default_config() -> Config:
|
|
|
99
107
|
)
|
|
100
108
|
|
|
101
109
|
|
|
102
|
-
def load_config() -> Config:
|
|
103
|
-
"""
|
|
110
|
+
def load_config(config_file: Path | None = None) -> Config:
|
|
111
|
+
"""
|
|
112
|
+
Load configuration from config file.
|
|
113
|
+
If the config file does not exist, create it with default configuration.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
104
117
|
|
|
105
118
|
Returns:
|
|
106
119
|
Validated Config object.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ConfigError: If the configuration file is invalid.
|
|
107
123
|
"""
|
|
108
|
-
config_file = get_config_file()
|
|
124
|
+
config_file = config_file or get_config_file()
|
|
109
125
|
logger.debug("Loading config from file: {file}", file=config_file)
|
|
110
126
|
|
|
111
127
|
if not config_file.exists():
|
|
@@ -119,20 +135,21 @@ def load_config() -> Config:
|
|
|
119
135
|
with open(config_file, encoding="utf-8") as f:
|
|
120
136
|
data = json.load(f)
|
|
121
137
|
return Config(**data)
|
|
122
|
-
except
|
|
123
|
-
raise ConfigError(f"Invalid configuration file: {
|
|
124
|
-
|
|
138
|
+
except json.JSONDecodeError as e:
|
|
139
|
+
raise ConfigError(f"Invalid JSON in configuration file: {e}") from e
|
|
140
|
+
except ValidationError as e:
|
|
141
|
+
raise ConfigError(f"Invalid configuration file: {e}") from e
|
|
125
142
|
|
|
126
|
-
class ConfigError(Exception):
|
|
127
|
-
"""Configuration error."""
|
|
128
|
-
|
|
129
|
-
def __init__(self, message: str):
|
|
130
|
-
super().__init__(message)
|
|
131
143
|
|
|
144
|
+
def save_config(config: Config, config_file: Path | None = None):
|
|
145
|
+
"""
|
|
146
|
+
Save configuration to config file.
|
|
132
147
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
Args:
|
|
149
|
+
config (Config): Config object to save.
|
|
150
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
151
|
+
"""
|
|
152
|
+
config_file = config_file or get_config_file()
|
|
136
153
|
logger.debug("Saving config to file: {file}", file=config_file)
|
|
137
154
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
138
155
|
f.write(config.model_dump_json(indent=2, exclude_none=True))
|
kimi_cli/constant.py
ADDED
kimi_cli/exception.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class KimiCLIException(Exception):
|
|
2
|
+
"""Base exception class for Kimi CLI."""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConfigError(KimiCLIException):
|
|
8
|
+
"""Configuration error."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentSpecError(KimiCLIException):
|
|
14
|
+
"""Agent specification error."""
|
|
15
|
+
|
|
16
|
+
pass
|