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.
Files changed (88) hide show
  1. kimi_cli/CHANGELOG.md +165 -0
  2. kimi_cli/__init__.py +0 -374
  3. kimi_cli/agents/{koder → default}/agent.yaml +1 -1
  4. kimi_cli/agents/{koder → default}/system.md +1 -1
  5. kimi_cli/agentspec.py +115 -0
  6. kimi_cli/app.py +208 -0
  7. kimi_cli/cli.py +321 -0
  8. kimi_cli/config.py +33 -16
  9. kimi_cli/constant.py +4 -0
  10. kimi_cli/exception.py +16 -0
  11. kimi_cli/llm.py +144 -3
  12. kimi_cli/metadata.py +6 -69
  13. kimi_cli/prompts/__init__.py +4 -0
  14. kimi_cli/session.py +103 -0
  15. kimi_cli/soul/__init__.py +130 -9
  16. kimi_cli/soul/agent.py +159 -0
  17. kimi_cli/soul/approval.py +5 -6
  18. kimi_cli/soul/compaction.py +106 -0
  19. kimi_cli/soul/context.py +1 -1
  20. kimi_cli/soul/kimisoul.py +180 -80
  21. kimi_cli/soul/message.py +6 -6
  22. kimi_cli/soul/runtime.py +96 -0
  23. kimi_cli/soul/toolset.py +3 -2
  24. kimi_cli/tools/__init__.py +35 -31
  25. kimi_cli/tools/bash/__init__.py +25 -9
  26. kimi_cli/tools/bash/cmd.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +5 -4
  28. kimi_cli/tools/file/__init__.py +8 -0
  29. kimi_cli/tools/file/glob.md +1 -1
  30. kimi_cli/tools/file/glob.py +4 -4
  31. kimi_cli/tools/file/grep.py +36 -19
  32. kimi_cli/tools/file/patch.py +52 -10
  33. kimi_cli/tools/file/read.py +6 -5
  34. kimi_cli/tools/file/replace.py +16 -4
  35. kimi_cli/tools/file/write.py +16 -4
  36. kimi_cli/tools/mcp.py +7 -4
  37. kimi_cli/tools/task/__init__.py +60 -41
  38. kimi_cli/tools/task/task.md +1 -1
  39. kimi_cli/tools/todo/__init__.py +4 -2
  40. kimi_cli/tools/utils.py +1 -1
  41. kimi_cli/tools/web/fetch.py +2 -1
  42. kimi_cli/tools/web/search.py +13 -12
  43. kimi_cli/ui/__init__.py +0 -68
  44. kimi_cli/ui/acp/__init__.py +67 -38
  45. kimi_cli/ui/print/__init__.py +46 -69
  46. kimi_cli/ui/shell/__init__.py +145 -154
  47. kimi_cli/ui/shell/console.py +27 -1
  48. kimi_cli/ui/shell/debug.py +187 -0
  49. kimi_cli/ui/shell/keyboard.py +183 -0
  50. kimi_cli/ui/shell/metacmd.py +34 -81
  51. kimi_cli/ui/shell/prompt.py +245 -28
  52. kimi_cli/ui/shell/replay.py +104 -0
  53. kimi_cli/ui/shell/setup.py +19 -19
  54. kimi_cli/ui/shell/update.py +11 -5
  55. kimi_cli/ui/shell/visualize.py +576 -0
  56. kimi_cli/ui/wire/README.md +109 -0
  57. kimi_cli/ui/wire/__init__.py +340 -0
  58. kimi_cli/ui/wire/jsonrpc.py +48 -0
  59. kimi_cli/utils/__init__.py +0 -0
  60. kimi_cli/utils/aiohttp.py +10 -0
  61. kimi_cli/utils/changelog.py +6 -2
  62. kimi_cli/utils/clipboard.py +10 -0
  63. kimi_cli/utils/message.py +15 -1
  64. kimi_cli/utils/rich/__init__.py +33 -0
  65. kimi_cli/utils/rich/markdown.py +959 -0
  66. kimi_cli/utils/rich/markdown_sample.md +108 -0
  67. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  68. kimi_cli/utils/signals.py +41 -0
  69. kimi_cli/utils/string.py +8 -0
  70. kimi_cli/utils/term.py +114 -0
  71. kimi_cli/wire/__init__.py +73 -0
  72. kimi_cli/wire/message.py +191 -0
  73. kimi_cli-0.52.dist-info/METADATA +186 -0
  74. kimi_cli-0.52.dist-info/RECORD +99 -0
  75. kimi_cli-0.52.dist-info/entry_points.txt +3 -0
  76. kimi_cli/agent.py +0 -261
  77. kimi_cli/agents/koder/README.md +0 -3
  78. kimi_cli/prompts/metacmds/__init__.py +0 -4
  79. kimi_cli/soul/wire.py +0 -101
  80. kimi_cli/ui/shell/liveview.py +0 -158
  81. kimi_cli/utils/provider.py +0 -64
  82. kimi_cli-0.35.dist-info/METADATA +0 -24
  83. kimi_cli-0.35.dist-info/RECORD +0 -76
  84. kimi_cli-0.35.dist-info/entry_points.txt +0 -3
  85. /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
  86. /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
  87. /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
  88. {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 Literal, Self
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: Literal["kimi", "openai_legacy", "_chaos"]
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
- """Load configuration from config file.
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 (json.JSONDecodeError, ValidationError) as e:
123
- raise ConfigError(f"Invalid configuration file: {config_file}") from e
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
- def save_config(config: Config):
134
- """Save configuration to config file."""
135
- config_file = get_config_file()
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
@@ -0,0 +1,4 @@
1
+ import importlib.metadata
2
+
3
+ VERSION = importlib.metadata.version("kimi-cli")
4
+ USER_AGENT = f"KimiCLI/{VERSION}"
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