kimi-cli 0.41__py3-none-any.whl → 0.42__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 +15 -0
- kimi_cli/__init__.py +131 -101
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agentspec.py +19 -8
- kimi_cli/cli.py +51 -37
- kimi_cli/config.py +28 -14
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +1 -0
- kimi_cli/metadata.py +5 -68
- kimi_cli/session.py +81 -0
- kimi_cli/soul/agent.py +18 -23
- kimi_cli/soul/context.py +0 -5
- kimi_cli/soul/kimisoul.py +19 -21
- kimi_cli/soul/{globals.py → runtime.py} +12 -10
- kimi_cli/tools/file/glob.py +1 -1
- kimi_cli/tools/file/patch.py +1 -1
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +1 -1
- kimi_cli/tools/file/write.py +1 -1
- kimi_cli/tools/task/__init__.py +28 -21
- kimi_cli/tools/web/search.py +3 -0
- kimi_cli/ui/print/__init__.py +4 -2
- kimi_cli/ui/shell/__init__.py +3 -6
- kimi_cli/ui/shell/metacmd.py +3 -8
- kimi_cli/ui/shell/prompt.py +10 -3
- kimi_cli/wire/__init__.py +12 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.42.dist-info}/METADATA +18 -18
- {kimi_cli-0.41.dist-info → kimi_cli-0.42.dist-info}/RECORD +32 -31
- kimi_cli/agents/koder/README.md +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/agents/{koder → default}/system.md +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.42.dist-info}/WHEEL +0 -0
- {kimi_cli-0.41.dist-info → kimi_cli-0.42.dist-info}/entry_points.txt +0 -0
kimi_cli/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,21 @@ Internal builds may append content to the Unreleased section.
|
|
|
9
9
|
Only write entries that are worth mentioning to users.
|
|
10
10
|
-->
|
|
11
11
|
|
|
12
|
+
## [0.42] - 2025-10-28
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Support Ctrl-J or Alt-Enter to insert a new line
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Change mode switch shortcut from Ctrl-K to Ctrl-X
|
|
21
|
+
- Improve overall robustness
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fix ACP server `no attribute` error
|
|
26
|
+
|
|
12
27
|
## [0.41] - 2025-10-26
|
|
13
28
|
|
|
14
29
|
### Fixed
|
kimi_cli/__init__.py
CHANGED
|
@@ -1,125 +1,155 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import os
|
|
3
3
|
import warnings
|
|
4
|
+
from collections.abc import Generator
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
|
-
import click
|
|
8
8
|
from pydantic import SecretStr
|
|
9
9
|
|
|
10
10
|
from kimi_cli.agentspec import DEFAULT_AGENT_FILE
|
|
11
|
-
from kimi_cli.config import
|
|
11
|
+
from kimi_cli.config import LLMModel, LLMProvider, load_config
|
|
12
12
|
from kimi_cli.llm import augment_provider_with_env_vars, create_llm
|
|
13
|
-
from kimi_cli.
|
|
14
|
-
from kimi_cli.soul.agent import
|
|
13
|
+
from kimi_cli.session import Session
|
|
14
|
+
from kimi_cli.soul.agent import load_agent
|
|
15
15
|
from kimi_cli.soul.context import Context
|
|
16
|
-
from kimi_cli.soul.globals import AgentGlobals
|
|
17
16
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
17
|
+
from kimi_cli.soul.runtime import Runtime
|
|
18
18
|
from kimi_cli.ui.acp import ACPServer
|
|
19
19
|
from kimi_cli.ui.print import InputFormat, OutputFormat, PrintApp
|
|
20
20
|
from kimi_cli.ui.shell import ShellApp
|
|
21
21
|
from kimi_cli.utils.logging import StreamToLogger, logger
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
model
|
|
56
|
-
provider
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
23
|
+
|
|
24
|
+
class KimiCLI:
|
|
25
|
+
@staticmethod
|
|
26
|
+
async def create(
|
|
27
|
+
session: Session,
|
|
28
|
+
*,
|
|
29
|
+
yolo: bool = False,
|
|
30
|
+
stream: bool = True, # TODO: remove this when we have a correct print mode impl
|
|
31
|
+
mcp_configs: list[dict[str, Any]] | None = None,
|
|
32
|
+
config_file: Path | None = None,
|
|
33
|
+
model_name: str | None = None,
|
|
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
|
+
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
|
+
return KimiCLI(soul, session)
|
|
99
|
+
|
|
100
|
+
def __init__(self, soul: KimiSoul, session: Session) -> None:
|
|
101
|
+
self._soul = soul
|
|
102
|
+
self._session = session
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def soul(self) -> KimiSoul:
|
|
106
|
+
"""Get the KimiSoul instance."""
|
|
107
|
+
return self._soul
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def session(self) -> Session:
|
|
111
|
+
"""Get the Session instance."""
|
|
112
|
+
return self._session
|
|
113
|
+
|
|
114
|
+
@contextlib.contextmanager
|
|
115
|
+
def _app_env(self) -> Generator[None]:
|
|
116
|
+
original_cwd = Path.cwd()
|
|
117
|
+
os.chdir(self._session.work_dir)
|
|
118
|
+
try:
|
|
119
|
+
# to ignore possible warnings from dateparser
|
|
120
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
121
|
+
with contextlib.redirect_stderr(StreamToLogger()):
|
|
122
|
+
yield
|
|
123
|
+
finally:
|
|
124
|
+
os.chdir(original_cwd)
|
|
125
|
+
|
|
126
|
+
async def run_shell_mode(self, command: str | None = None) -> bool:
|
|
127
|
+
with self._app_env():
|
|
97
128
|
app = ShellApp(
|
|
98
|
-
|
|
129
|
+
self._soul,
|
|
99
130
|
welcome_info={
|
|
100
|
-
"Directory": str(work_dir),
|
|
101
|
-
"Session":
|
|
131
|
+
"Directory": str(self._session.work_dir),
|
|
132
|
+
"Session": self._session.id,
|
|
102
133
|
},
|
|
103
134
|
)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
return await app.run(command)
|
|
136
|
+
|
|
137
|
+
async def run_print_mode(
|
|
138
|
+
self,
|
|
139
|
+
input_format: InputFormat,
|
|
140
|
+
output_format: OutputFormat,
|
|
141
|
+
command: str | None = None,
|
|
142
|
+
) -> bool:
|
|
143
|
+
with self._app_env():
|
|
110
144
|
app = PrintApp(
|
|
111
|
-
|
|
112
|
-
input_format
|
|
113
|
-
output_format
|
|
114
|
-
|
|
145
|
+
self._soul,
|
|
146
|
+
input_format,
|
|
147
|
+
output_format,
|
|
148
|
+
self._session.history_file,
|
|
115
149
|
)
|
|
116
150
|
return await app.run(command)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
app = ACPServer(
|
|
151
|
+
|
|
152
|
+
async def run_acp_server(self) -> bool:
|
|
153
|
+
with self._app_env():
|
|
154
|
+
app = ACPServer(self._soul)
|
|
121
155
|
return await app.run()
|
|
122
|
-
else:
|
|
123
|
-
raise click.BadParameter(f"Invalid UI mode: {ui}")
|
|
124
|
-
finally:
|
|
125
|
-
os.chdir(original_cwd)
|
kimi_cli/agentspec.py
CHANGED
|
@@ -4,12 +4,14 @@ from typing import Any, NamedTuple
|
|
|
4
4
|
import yaml
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
+
from kimi_cli.exception import AgentSpecError
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
def get_agents_dir() -> Path:
|
|
9
11
|
return Path(__file__).parent / "agents"
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
DEFAULT_AGENT_FILE = get_agents_dir() / "
|
|
14
|
+
DEFAULT_AGENT_FILE = get_agents_dir() / "default" / "agent.yaml"
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class AgentSpec(BaseModel):
|
|
@@ -47,15 +49,21 @@ class ResolvedAgentSpec(NamedTuple):
|
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
|
|
50
|
-
"""
|
|
52
|
+
"""
|
|
53
|
+
Load agent specification from file.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
FileNotFoundError: If the agent spec file is not found.
|
|
57
|
+
AgentSpecError: If the agent spec is not valid.
|
|
58
|
+
"""
|
|
51
59
|
agent_spec = _load_agent_spec(agent_file)
|
|
52
60
|
assert agent_spec.extend is None, "agent extension should be recursively resolved"
|
|
53
61
|
if agent_spec.name is None:
|
|
54
|
-
raise
|
|
62
|
+
raise AgentSpecError("Agent name is required")
|
|
55
63
|
if agent_spec.system_prompt_path is None:
|
|
56
|
-
raise
|
|
64
|
+
raise AgentSpecError("System prompt path is required")
|
|
57
65
|
if agent_spec.tools is None:
|
|
58
|
-
raise
|
|
66
|
+
raise AgentSpecError("Tools are required")
|
|
59
67
|
return ResolvedAgentSpec(
|
|
60
68
|
name=agent_spec.name,
|
|
61
69
|
system_prompt_path=agent_spec.system_prompt_path,
|
|
@@ -68,12 +76,15 @@ def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
|
|
|
68
76
|
|
|
69
77
|
def _load_agent_spec(agent_file: Path) -> AgentSpec:
|
|
70
78
|
assert agent_file.is_file(), "expect agent file to exist"
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
try:
|
|
80
|
+
with open(agent_file, encoding="utf-8") as f:
|
|
81
|
+
data: dict[str, Any] = yaml.safe_load(f)
|
|
82
|
+
except yaml.YAMLError as e:
|
|
83
|
+
raise AgentSpecError(f"Invalid YAML in agent spec file: {e}") from e
|
|
73
84
|
|
|
74
85
|
version = data.get("version", 1)
|
|
75
86
|
if version != 1:
|
|
76
|
-
raise
|
|
87
|
+
raise AgentSpecError(f"Unsupported agent spec version: {version}")
|
|
77
88
|
|
|
78
89
|
agent_spec = AgentSpec(**data.get("agent", {}))
|
|
79
90
|
if agent_spec.system_prompt_path is not None:
|
kimi_cli/cli.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import sys
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal, get_args
|
|
5
7
|
|
|
6
8
|
import click
|
|
7
9
|
|
|
8
|
-
from kimi_cli import
|
|
9
|
-
from kimi_cli.agentspec import DEFAULT_AGENT_FILE
|
|
10
|
-
from kimi_cli.config import ConfigError, load_config
|
|
10
|
+
from kimi_cli import KimiCLI
|
|
11
11
|
from kimi_cli.constant import VERSION
|
|
12
|
-
from kimi_cli.
|
|
12
|
+
from kimi_cli.session import Session
|
|
13
13
|
from kimi_cli.share import get_share_dir
|
|
14
14
|
from kimi_cli.ui.print import InputFormat, OutputFormat
|
|
15
15
|
from kimi_cli.utils.logging import logger
|
|
@@ -21,6 +21,9 @@ class Reload(Exception):
|
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
UIMode = Literal["shell", "print", "acp"]
|
|
25
|
+
|
|
26
|
+
|
|
24
27
|
@click.command(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
25
28
|
@click.version_option(VERSION)
|
|
26
29
|
@click.option(
|
|
@@ -38,8 +41,8 @@ class Reload(Exception):
|
|
|
38
41
|
@click.option(
|
|
39
42
|
"--agent-file",
|
|
40
43
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
|
41
|
-
default=
|
|
42
|
-
help="Custom agent specification file. Default: builtin
|
|
44
|
+
default=None,
|
|
45
|
+
help="Custom agent specification file. Default: builtin default agent.",
|
|
43
46
|
)
|
|
44
47
|
@click.option(
|
|
45
48
|
"--model",
|
|
@@ -77,7 +80,7 @@ class Reload(Exception):
|
|
|
77
80
|
@click.option(
|
|
78
81
|
"--ui",
|
|
79
82
|
"ui",
|
|
80
|
-
type=click.Choice(
|
|
83
|
+
type=click.Choice(get_args(UIMode)),
|
|
81
84
|
default="shell",
|
|
82
85
|
help="UI mode to use. Default: shell.",
|
|
83
86
|
)
|
|
@@ -85,7 +88,7 @@ class Reload(Exception):
|
|
|
85
88
|
"--print",
|
|
86
89
|
"ui",
|
|
87
90
|
flag_value="print",
|
|
88
|
-
help="Run in print mode. Shortcut for `--ui print`.",
|
|
91
|
+
help="Run in print mode. Shortcut for `--ui print`. Note: print mode implicitly adds `--yolo`.",
|
|
89
92
|
)
|
|
90
93
|
@click.option(
|
|
91
94
|
"--acp",
|
|
@@ -95,7 +98,7 @@ class Reload(Exception):
|
|
|
95
98
|
)
|
|
96
99
|
@click.option(
|
|
97
100
|
"--input-format",
|
|
98
|
-
type=click.Choice(
|
|
101
|
+
type=click.Choice(get_args(InputFormat)),
|
|
99
102
|
default=None,
|
|
100
103
|
help=(
|
|
101
104
|
"Input format to use. Must be used with `--print` "
|
|
@@ -105,7 +108,7 @@ class Reload(Exception):
|
|
|
105
108
|
)
|
|
106
109
|
@click.option(
|
|
107
110
|
"--output-format",
|
|
108
|
-
type=click.Choice(
|
|
111
|
+
type=click.Choice(get_args(OutputFormat)),
|
|
109
112
|
default=None,
|
|
110
113
|
help="Output format to use. Must be used with `--print`. Default: text.",
|
|
111
114
|
)
|
|
@@ -140,7 +143,7 @@ class Reload(Exception):
|
|
|
140
143
|
def kimi(
|
|
141
144
|
verbose: bool,
|
|
142
145
|
debug: bool,
|
|
143
|
-
agent_file: Path,
|
|
146
|
+
agent_file: Path | None,
|
|
144
147
|
model_name: str | None,
|
|
145
148
|
work_dir: Path,
|
|
146
149
|
continue_: bool,
|
|
@@ -153,7 +156,11 @@ def kimi(
|
|
|
153
156
|
yolo: bool,
|
|
154
157
|
):
|
|
155
158
|
"""Kimi, your next CLI agent."""
|
|
156
|
-
|
|
159
|
+
|
|
160
|
+
def _noop_echo(*args: Any, **kwargs: Any):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
echo: Callable[..., None] = click.echo if verbose else _noop_echo
|
|
157
164
|
|
|
158
165
|
logger.add(
|
|
159
166
|
get_share_dir() / "logs" / "kimi.log",
|
|
@@ -163,19 +170,23 @@ def kimi(
|
|
|
163
170
|
)
|
|
164
171
|
|
|
165
172
|
work_dir = work_dir.absolute()
|
|
166
|
-
|
|
167
173
|
if continue_:
|
|
168
|
-
session =
|
|
174
|
+
session = Session.continue_(work_dir)
|
|
169
175
|
if session is None:
|
|
170
176
|
raise click.BadOptionUsage(
|
|
171
177
|
"--continue", "No previous session found for the working directory"
|
|
172
178
|
)
|
|
173
179
|
echo(f"✓ Continuing previous session: {session.id}")
|
|
174
180
|
else:
|
|
175
|
-
session =
|
|
181
|
+
session = Session.create(work_dir)
|
|
176
182
|
echo(f"✓ Created new session: {session.id}")
|
|
177
183
|
echo(f"✓ Session history file: {session.history_file}")
|
|
178
184
|
|
|
185
|
+
if command is not None:
|
|
186
|
+
command = command.strip()
|
|
187
|
+
if not command:
|
|
188
|
+
raise click.BadOptionUsage("--command", "Command cannot be empty")
|
|
189
|
+
|
|
179
190
|
if input_format is not None and ui != "print":
|
|
180
191
|
raise click.BadOptionUsage(
|
|
181
192
|
"--input-format",
|
|
@@ -188,7 +199,7 @@ def kimi(
|
|
|
188
199
|
)
|
|
189
200
|
|
|
190
201
|
try:
|
|
191
|
-
mcp_configs = [json.loads(conf.read_text()) for conf in mcp_config_file]
|
|
202
|
+
mcp_configs = [json.loads(conf.read_text(encoding="utf-8")) for conf in mcp_config_file]
|
|
192
203
|
except json.JSONDecodeError as e:
|
|
193
204
|
raise click.BadOptionUsage("--mcp-config-file", f"Invalid JSON: {e}") from e
|
|
194
205
|
|
|
@@ -197,29 +208,32 @@ def kimi(
|
|
|
197
208
|
except json.JSONDecodeError as e:
|
|
198
209
|
raise click.BadOptionUsage("--mcp-config", f"Invalid JSON: {e}") from e
|
|
199
210
|
|
|
211
|
+
async def _run() -> bool:
|
|
212
|
+
instance = await KimiCLI.create(
|
|
213
|
+
session,
|
|
214
|
+
yolo=yolo or (ui == "print"), # print mode implies yolo
|
|
215
|
+
stream=ui != "print", # use non-streaming mode only for print UI
|
|
216
|
+
mcp_configs=mcp_configs,
|
|
217
|
+
model_name=model_name,
|
|
218
|
+
agent_file=agent_file,
|
|
219
|
+
)
|
|
220
|
+
match ui:
|
|
221
|
+
case "shell":
|
|
222
|
+
return await instance.run_shell_mode(command)
|
|
223
|
+
case "print":
|
|
224
|
+
return await instance.run_print_mode(
|
|
225
|
+
input_format or "text",
|
|
226
|
+
output_format or "text",
|
|
227
|
+
command,
|
|
228
|
+
)
|
|
229
|
+
case "acp":
|
|
230
|
+
if command is not None:
|
|
231
|
+
logger.warning("ACP server ignores command argument")
|
|
232
|
+
return await instance.run_acp_server()
|
|
233
|
+
|
|
200
234
|
while True:
|
|
201
235
|
try:
|
|
202
|
-
|
|
203
|
-
config = load_config()
|
|
204
|
-
except ConfigError as e:
|
|
205
|
-
raise click.ClickException(f"Failed to load config: {e}") from e
|
|
206
|
-
echo(f"✓ Loaded config: {config}")
|
|
207
|
-
|
|
208
|
-
succeeded = asyncio.run(
|
|
209
|
-
kimi_run(
|
|
210
|
-
config=config,
|
|
211
|
-
model_name=model_name,
|
|
212
|
-
work_dir=work_dir,
|
|
213
|
-
session=session,
|
|
214
|
-
command=command,
|
|
215
|
-
agent_file=agent_file,
|
|
216
|
-
ui=ui,
|
|
217
|
-
input_format=input_format,
|
|
218
|
-
output_format=output_format,
|
|
219
|
-
mcp_configs=mcp_configs,
|
|
220
|
-
yolo=yolo,
|
|
221
|
-
)
|
|
222
|
-
)
|
|
236
|
+
succeeded = asyncio.run(_run())
|
|
223
237
|
if not succeeded:
|
|
224
238
|
sys.exit(1)
|
|
225
239
|
break
|
kimi_cli/config.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Literal, 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
|
|
7
8
|
from kimi_cli.share import get_share_dir
|
|
8
9
|
from kimi_cli.utils.logging import logger
|
|
9
10
|
|
|
@@ -17,6 +18,8 @@ class LLMProvider(BaseModel):
|
|
|
17
18
|
"""API base URL"""
|
|
18
19
|
api_key: SecretStr
|
|
19
20
|
"""API key"""
|
|
21
|
+
custom_headers: dict[str, str] = Field(default_factory=dict)
|
|
22
|
+
"""Custom headers to include in API requests"""
|
|
20
23
|
|
|
21
24
|
@field_serializer("api_key", when_used="json")
|
|
22
25
|
def dump_secret(self, v: SecretStr):
|
|
@@ -50,6 +53,8 @@ class MoonshotSearchConfig(BaseModel):
|
|
|
50
53
|
"""Base URL for Moonshot Search service."""
|
|
51
54
|
api_key: SecretStr
|
|
52
55
|
"""API key for Moonshot Search service."""
|
|
56
|
+
custom_headers: dict[str, str] = Field(default_factory=dict)
|
|
57
|
+
"""Custom headers to include in API requests."""
|
|
53
58
|
|
|
54
59
|
@field_serializer("api_key", when_used="json")
|
|
55
60
|
def dump_secret(self, v: SecretStr):
|
|
@@ -99,13 +104,21 @@ def get_default_config() -> Config:
|
|
|
99
104
|
)
|
|
100
105
|
|
|
101
106
|
|
|
102
|
-
def load_config() -> Config:
|
|
103
|
-
"""
|
|
107
|
+
def load_config(config_file: Path | None = None) -> Config:
|
|
108
|
+
"""
|
|
109
|
+
Load configuration from config file.
|
|
110
|
+
If the config file does not exist, create it with default configuration.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
104
114
|
|
|
105
115
|
Returns:
|
|
106
116
|
Validated Config object.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ConfigError: If the configuration file is invalid.
|
|
107
120
|
"""
|
|
108
|
-
config_file = get_config_file()
|
|
121
|
+
config_file = config_file or get_config_file()
|
|
109
122
|
logger.debug("Loading config from file: {file}", file=config_file)
|
|
110
123
|
|
|
111
124
|
if not config_file.exists():
|
|
@@ -119,20 +132,21 @@ def load_config() -> Config:
|
|
|
119
132
|
with open(config_file, encoding="utf-8") as f:
|
|
120
133
|
data = json.load(f)
|
|
121
134
|
return Config(**data)
|
|
122
|
-
except
|
|
123
|
-
raise ConfigError(f"Invalid configuration file: {
|
|
124
|
-
|
|
135
|
+
except json.JSONDecodeError as e:
|
|
136
|
+
raise ConfigError(f"Invalid JSON in configuration file: {e}") from e
|
|
137
|
+
except ValidationError as e:
|
|
138
|
+
raise ConfigError(f"Invalid configuration file: {e}") from e
|
|
125
139
|
|
|
126
|
-
class ConfigError(Exception):
|
|
127
|
-
"""Configuration error."""
|
|
128
|
-
|
|
129
|
-
def __init__(self, message: str):
|
|
130
|
-
super().__init__(message)
|
|
131
140
|
|
|
141
|
+
def save_config(config: Config, config_file: Path | None = None):
|
|
142
|
+
"""
|
|
143
|
+
Save configuration to config file.
|
|
132
144
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
Args:
|
|
146
|
+
config (Config): Config object to save.
|
|
147
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
148
|
+
"""
|
|
149
|
+
config_file = config_file or get_config_file()
|
|
136
150
|
logger.debug("Saving config to file: {file}", file=config_file)
|
|
137
151
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
138
152
|
f.write(config.model_dump_json(indent=2, exclude_none=True))
|
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
|