kimi-cli 0.43__tar.gz → 0.47__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kimi-cli might be problematic. Click here for more details.

Files changed (93) hide show
  1. {kimi_cli-0.43 → kimi_cli-0.47}/PKG-INFO +4 -3
  2. {kimi_cli-0.43 → kimi_cli-0.47}/README.md +1 -0
  3. {kimi_cli-0.43 → kimi_cli-0.47}/pyproject.toml +3 -3
  4. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/CHANGELOG.md +40 -0
  5. kimi_cli-0.43/src/kimi_cli/__init__.py → kimi_cli-0.47/src/kimi_cli/app.py +62 -52
  6. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/cli.py +23 -8
  7. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/config.py +1 -1
  8. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/llm.py +41 -7
  9. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/agent.py +10 -3
  10. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/runtime.py +6 -4
  11. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/toolset.py +2 -1
  12. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/__init__.py +19 -10
  13. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/bash/__init__.py +13 -4
  14. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/dmail/__init__.py +3 -3
  15. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/glob.py +2 -2
  16. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/grep.py +1 -1
  17. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/patch.py +38 -8
  18. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/read.py +5 -4
  19. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/replace.py +2 -2
  20. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/write.py +2 -2
  21. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/mcp.py +6 -3
  22. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/task/__init__.py +2 -2
  23. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/web/search.py +8 -10
  24. kimi_cli-0.47/src/kimi_cli/ui/__init__.py +0 -0
  25. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/print/__init__.py +1 -4
  26. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/__init__.py +11 -2
  27. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/liveview.py +36 -5
  28. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/prompt.py +29 -16
  29. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/setup.py +1 -1
  30. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/visualize.py +7 -3
  31. kimi_cli-0.47/src/kimi_cli/ui/wire/README.md +109 -0
  32. kimi_cli-0.47/src/kimi_cli/ui/wire/__init__.py +340 -0
  33. kimi_cli-0.47/src/kimi_cli/ui/wire/jsonrpc.py +48 -0
  34. kimi_cli-0.47/src/kimi_cli/utils/clipboard.py +10 -0
  35. kimi_cli-0.47/src/kimi_cli/wire/message.py +178 -0
  36. kimi_cli-0.43/src/kimi_cli/wire/message.py +0 -91
  37. {kimi_cli-0.43/src/kimi_cli/ui → kimi_cli-0.47/src/kimi_cli}/__init__.py +0 -0
  38. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/agents/default/agent.yaml +0 -0
  39. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/agents/default/sub.yaml +0 -0
  40. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/agents/default/system.md +0 -0
  41. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/agentspec.py +0 -0
  42. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/constant.py +0 -0
  43. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/exception.py +0 -0
  44. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/metadata.py +0 -0
  45. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/prompts/__init__.py +0 -0
  46. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/prompts/compact.md +0 -0
  47. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/prompts/init.md +0 -0
  48. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/py.typed +0 -0
  49. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/session.py +0 -0
  50. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/share.py +0 -0
  51. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/__init__.py +0 -0
  52. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/approval.py +0 -0
  53. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/compaction.py +0 -0
  54. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/context.py +0 -0
  55. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/denwarenji.py +0 -0
  56. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/kimisoul.py +0 -0
  57. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/soul/message.py +0 -0
  58. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/bash/bash.md +0 -0
  59. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/dmail/dmail.md +0 -0
  60. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/__init__.py +0 -0
  61. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/glob.md +0 -0
  62. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/grep.md +0 -0
  63. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/patch.md +0 -0
  64. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/read.md +0 -0
  65. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/replace.md +0 -0
  66. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/file/write.md +0 -0
  67. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/task/task.md +0 -0
  68. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/test.py +0 -0
  69. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/think/__init__.py +0 -0
  70. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/think/think.md +0 -0
  71. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/todo/__init__.py +0 -0
  72. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/todo/set_todo_list.md +0 -0
  73. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/utils.py +0 -0
  74. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/web/__init__.py +0 -0
  75. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/web/fetch.md +0 -0
  76. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/web/fetch.py +0 -0
  77. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/tools/web/search.md +0 -0
  78. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/acp/__init__.py +0 -0
  79. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/console.py +0 -0
  80. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/debug.py +0 -0
  81. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/keyboard.py +0 -0
  82. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/metacmd.py +0 -0
  83. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/replay.py +0 -0
  84. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/ui/shell/update.py +0 -0
  85. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/aiohttp.py +0 -0
  86. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/changelog.py +0 -0
  87. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/logging.py +0 -0
  88. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/message.py +0 -0
  89. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/path.py +0 -0
  90. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/pyinstaller.py +0 -0
  91. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/signals.py +0 -0
  92. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/utils/string.py +0 -0
  93. {kimi_cli-0.43 → kimi_cli-0.47}/src/kimi_cli/wire/__init__.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kimi-cli
3
- Version: 0.43
3
+ Version: 0.47
4
4
  Summary: Kimi CLI is your next CLI agent.
5
- Requires-Dist: agent-client-protocol==0.6.2
5
+ Requires-Dist: agent-client-protocol==0.6.3
6
6
  Requires-Dist: aiofiles==25.1.0
7
7
  Requires-Dist: aiohttp==3.13.2
8
8
  Requires-Dist: click==8.3.0
9
- Requires-Dist: kosong==0.16.1
9
+ Requires-Dist: kosong==0.17.0
10
10
  Requires-Dist: loguru==0.7.3
11
11
  Requires-Dist: patch-ng==1.19.0
12
12
  Requires-Dist: prompt-toolkit==3.0.52
@@ -29,6 +29,7 @@ Description-Content-Type: text/markdown
29
29
  [![Checks](https://img.shields.io/github/check-runs/MoonshotAI/kimi-cli/main)](https://github.com/MoonshotAI/kimi-cli/actions)
30
30
  [![Version](https://img.shields.io/pypi/v/kimi-cli)](https://pypi.org/project/kimi-cli/)
31
31
  [![Downloads](https://img.shields.io/pypi/dw/kimi-cli)](https://pypistats.org/packages/kimi-cli)
32
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/MoonshotAI/kimi-cli)
32
33
 
33
34
  [中文](https://www.kimi.com/coding/docs/kimi-cli.html)
34
35
 
@@ -4,6 +4,7 @@
4
4
  [![Checks](https://img.shields.io/github/check-runs/MoonshotAI/kimi-cli/main)](https://github.com/MoonshotAI/kimi-cli/actions)
5
5
  [![Version](https://img.shields.io/pypi/v/kimi-cli)](https://pypi.org/project/kimi-cli/)
6
6
  [![Downloads](https://img.shields.io/pypi/dw/kimi-cli)](https://pypistats.org/packages/kimi-cli)
7
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/MoonshotAI/kimi-cli)
7
8
 
8
9
  [中文](https://www.kimi.com/coding/docs/kimi-cli.html)
9
10
 
@@ -1,15 +1,15 @@
1
1
  [project]
2
2
  name = "kimi-cli"
3
- version = "0.43"
3
+ version = "0.47"
4
4
  description = "Kimi CLI is your next CLI agent."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  dependencies = [
8
- "agent-client-protocol==0.6.2",
8
+ "agent-client-protocol==0.6.3",
9
9
  "aiofiles==25.1.0",
10
10
  "aiohttp==3.13.2",
11
11
  "click==8.3.0",
12
- "kosong==0.16.1",
12
+ "kosong==0.17.0",
13
13
  "loguru==0.7.3",
14
14
  "patch-ng==1.19.0",
15
15
  "prompt-toolkit==3.0.52",
@@ -9,6 +9,46 @@ 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.47] - 2025-11-05
13
+
14
+ ### Fixed
15
+
16
+ - Fix Ctrl-W not working in some environments
17
+ - Do not load SearchWeb tool when the search service is not configured
18
+
19
+ ## [0.46] - 2025-11-03
20
+
21
+ ### Added
22
+
23
+ - Introduce Wire over stdio for local IPC (experimental, subject to change)
24
+ - Support Anthropic provider type
25
+
26
+ ### Fixed
27
+
28
+ - Fix binary packed by PyInstaller not working due to wrong entrypoint
29
+
30
+ ## [0.45] - 2025-10-31
31
+
32
+ ### Added
33
+
34
+ - Allow `KIMI_MODEL_CAPABILITIES` environment variable to override model capabilities
35
+ - Add `--no-markdown` option to disable markdown rendering
36
+ - Support `openai_responses` LLM provider type
37
+
38
+ ### Fixed
39
+
40
+ - Fix crash when continuing a session
41
+
42
+ ## [0.44] - 2025-10-30
43
+
44
+ ### Changed
45
+
46
+ - Improve startup time
47
+
48
+ ### Fixed
49
+
50
+ - Fix potential invalid bytes in user input
51
+
12
52
  ## [0.43] - 2025-10-30
13
53
 
14
54
  ### Added
@@ -8,6 +8,7 @@ from typing import Any
8
8
  from pydantic import SecretStr
9
9
 
10
10
  from kimi_cli.agentspec import DEFAULT_AGENT_FILE
11
+ from kimi_cli.cli import InputFormat, OutputFormat
11
12
  from kimi_cli.config import LLMModel, LLMProvider, load_config
12
13
  from kimi_cli.llm import augment_provider_with_env_vars, create_llm
13
14
  from kimi_cli.session import Session
@@ -15,9 +16,6 @@ from kimi_cli.soul.agent import load_agent
15
16
  from kimi_cli.soul.context import Context
16
17
  from kimi_cli.soul.kimisoul import KimiSoul
17
18
  from kimi_cli.soul.runtime import Runtime
18
- from kimi_cli.ui.acp import ACPServer
19
- from kimi_cli.ui.print import InputFormat, OutputFormat, PrintApp
20
- from kimi_cli.ui.shell import ShellApp, WelcomeInfoItem
21
19
  from kimi_cli.utils.logging import StreamToLogger, logger
22
20
 
23
21
 
@@ -81,43 +79,6 @@ class KimiCLI:
81
79
  logger.info("Using LLM model: {model}", model=model)
82
80
  llm = create_llm(provider, model, stream=stream, session_id=session.id)
83
81
 
84
- welcome_info = [
85
- WelcomeInfoItem(name="Directory", value=str(session.work_dir)),
86
- WelcomeInfoItem(name="Session", value=session.id),
87
- ]
88
- if base_url := env_overrides.get("KIMI_BASE_URL"):
89
- welcome_info.append(
90
- WelcomeInfoItem(
91
- name="API URL",
92
- value=f"{base_url} (from KIMI_BASE_URL)",
93
- level=WelcomeInfoItem.Level.WARN,
94
- )
95
- )
96
- if not llm:
97
- welcome_info.append(
98
- WelcomeInfoItem(
99
- name="Model",
100
- value="not set, send /setup to configure",
101
- level=WelcomeInfoItem.Level.WARN,
102
- )
103
- )
104
- elif "KIMI_MODEL_NAME" in env_overrides:
105
- welcome_info.append(
106
- WelcomeInfoItem(
107
- name="Model",
108
- value=f"{model.model} (from KIMI_MODEL_NAME)",
109
- level=WelcomeInfoItem.Level.WARN,
110
- )
111
- )
112
- else:
113
- welcome_info.append(
114
- WelcomeInfoItem(
115
- name="Model",
116
- value=model.model,
117
- level=WelcomeInfoItem.Level.INFO,
118
- )
119
- )
120
-
121
82
  runtime = await Runtime.create(config, llm, session, yolo)
122
83
 
123
84
  if agent_file is None:
@@ -132,17 +93,17 @@ class KimiCLI:
132
93
  runtime,
133
94
  context=context,
134
95
  )
135
- return KimiCLI(soul, session, welcome_info)
96
+ return KimiCLI(soul, runtime, env_overrides)
136
97
 
137
98
  def __init__(
138
99
  self,
139
- soul: KimiSoul,
140
- session: Session,
141
- welcome_info: list[WelcomeInfoItem],
100
+ _soul: KimiSoul,
101
+ _runtime: Runtime,
102
+ _env_overrides: dict[str, str],
142
103
  ) -> None:
143
- self._soul = soul
144
- self._session = session
145
- self._welcome_info = welcome_info
104
+ self._soul = _soul
105
+ self._runtime = _runtime
106
+ self._env_overrides = _env_overrides
146
107
 
147
108
  @property
148
109
  def soul(self) -> KimiSoul:
@@ -152,12 +113,12 @@ class KimiCLI:
152
113
  @property
153
114
  def session(self) -> Session:
154
115
  """Get the Session instance."""
155
- return self._session
116
+ return self._runtime.session
156
117
 
157
118
  @contextlib.contextmanager
158
119
  def _app_env(self) -> Generator[None]:
159
120
  original_cwd = Path.cwd()
160
- os.chdir(self._session.work_dir)
121
+ os.chdir(self._runtime.session.work_dir)
161
122
  try:
162
123
  # to ignore possible warnings from dateparser
163
124
  warnings.filterwarnings("ignore", category=DeprecationWarning)
@@ -166,9 +127,47 @@ class KimiCLI:
166
127
  finally:
167
128
  os.chdir(original_cwd)
168
129
 
169
- async def run_shell_mode(self, command: str | None = None) -> bool:
130
+ async def run_shell_mode(self, command: str | None = None, markdown: bool = True) -> bool:
131
+ from kimi_cli.ui.shell import ShellApp, WelcomeInfoItem
132
+
133
+ welcome_info = [
134
+ WelcomeInfoItem(name="Directory", value=str(self._runtime.session.work_dir)),
135
+ WelcomeInfoItem(name="Session", value=self._runtime.session.id),
136
+ ]
137
+ if base_url := self._env_overrides.get("KIMI_BASE_URL"):
138
+ welcome_info.append(
139
+ WelcomeInfoItem(
140
+ name="API URL",
141
+ value=f"{base_url} (from KIMI_BASE_URL)",
142
+ level=WelcomeInfoItem.Level.WARN,
143
+ )
144
+ )
145
+ if not self._runtime.llm:
146
+ welcome_info.append(
147
+ WelcomeInfoItem(
148
+ name="Model",
149
+ value="not set, send /setup to configure",
150
+ level=WelcomeInfoItem.Level.WARN,
151
+ )
152
+ )
153
+ elif "KIMI_MODEL_NAME" in self._env_overrides:
154
+ welcome_info.append(
155
+ WelcomeInfoItem(
156
+ name="Model",
157
+ value=f"{self._soul.model} (from KIMI_MODEL_NAME)",
158
+ level=WelcomeInfoItem.Level.WARN,
159
+ )
160
+ )
161
+ else:
162
+ welcome_info.append(
163
+ WelcomeInfoItem(
164
+ name="Model",
165
+ value=self._soul.model,
166
+ level=WelcomeInfoItem.Level.INFO,
167
+ )
168
+ )
170
169
  with self._app_env():
171
- app = ShellApp(self._soul, welcome_info=self._welcome_info)
170
+ app = ShellApp(self._soul, welcome_info=welcome_info, markdown=markdown)
172
171
  return await app.run(command)
173
172
 
174
173
  async def run_print_mode(
@@ -177,16 +176,27 @@ class KimiCLI:
177
176
  output_format: OutputFormat,
178
177
  command: str | None = None,
179
178
  ) -> bool:
179
+ from kimi_cli.ui.print import PrintApp
180
+
180
181
  with self._app_env():
181
182
  app = PrintApp(
182
183
  self._soul,
183
184
  input_format,
184
185
  output_format,
185
- self._session.history_file,
186
+ self._runtime.session.history_file,
186
187
  )
187
188
  return await app.run(command)
188
189
 
189
190
  async def run_acp_server(self) -> bool:
191
+ from kimi_cli.ui.acp import ACPServer
192
+
190
193
  with self._app_env():
191
194
  app = ACPServer(self._soul)
192
195
  return await app.run()
196
+
197
+ async def run_wire_server(self) -> bool:
198
+ from kimi_cli.ui.wire import WireServer
199
+
200
+ with self._app_env():
201
+ server = WireServer(self._soul)
202
+ return await server.run()
@@ -7,12 +7,7 @@ from typing import Any, Literal, get_args
7
7
 
8
8
  import click
9
9
 
10
- from kimi_cli import KimiCLI
11
10
  from kimi_cli.constant import VERSION
12
- from kimi_cli.session import Session
13
- from kimi_cli.share import get_share_dir
14
- from kimi_cli.ui.print import InputFormat, OutputFormat
15
- from kimi_cli.utils.logging import logger
16
11
 
17
12
 
18
13
  class Reload(Exception):
@@ -21,7 +16,9 @@ class Reload(Exception):
21
16
  pass
22
17
 
23
18
 
24
- UIMode = Literal["shell", "print", "acp"]
19
+ UIMode = Literal["shell", "print", "acp", "wire"]
20
+ InputFormat = Literal["text", "stream-json"]
21
+ OutputFormat = Literal["text", "stream-json"]
25
22
 
26
23
 
27
24
  @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
@@ -140,6 +137,12 @@ UIMode = Literal["shell", "print", "acp"]
140
137
  default=False,
141
138
  help="Automatically approve all actions. Default: no.",
142
139
  )
140
+ @click.option(
141
+ "--markdown/--no-markdown",
142
+ is_flag=True,
143
+ default=True,
144
+ help="Enable/disable markdown rendering in shell UI. Default: yes.",
145
+ )
143
146
  def kimi(
144
147
  verbose: bool,
145
148
  debug: bool,
@@ -154,17 +157,25 @@ def kimi(
154
157
  mcp_config_file: list[Path],
155
158
  mcp_config: list[str],
156
159
  yolo: bool,
160
+ markdown: bool,
157
161
  ):
158
162
  """Kimi, your next CLI agent."""
163
+ from kimi_cli.app import KimiCLI
164
+ from kimi_cli.session import Session
165
+ from kimi_cli.share import get_share_dir
166
+ from kimi_cli.utils.logging import logger
159
167
 
160
168
  def _noop_echo(*args: Any, **kwargs: Any):
161
169
  pass
162
170
 
163
171
  echo: Callable[..., None] = click.echo if verbose else _noop_echo
164
172
 
173
+ if debug:
174
+ logger.enable("kosong")
165
175
  logger.add(
166
176
  get_share_dir() / "logs" / "kimi.log",
167
- level="DEBUG" if debug else "INFO",
177
+ # FIXME: configure level for different modules
178
+ level="TRACE" if debug else "INFO",
168
179
  rotation="06:00",
169
180
  retention="10 days",
170
181
  )
@@ -219,7 +230,7 @@ def kimi(
219
230
  )
220
231
  match ui:
221
232
  case "shell":
222
- return await instance.run_shell_mode(command)
233
+ return await instance.run_shell_mode(command, markdown=markdown)
223
234
  case "print":
224
235
  return await instance.run_print_mode(
225
236
  input_format or "text",
@@ -230,6 +241,10 @@ def kimi(
230
241
  if command is not None:
231
242
  logger.warning("ACP server ignores command argument")
232
243
  return await instance.run_acp_server()
244
+ case "wire":
245
+ if command is not None:
246
+ logger.warning("Wire server ignores command argument")
247
+ return await instance.run_wire_server()
233
248
 
234
249
  while True:
235
250
  try:
@@ -12,7 +12,7 @@ from kimi_cli.utils.logging import logger
12
12
  class LLMProvider(BaseModel):
13
13
  """LLM provider configuration."""
14
14
 
15
- type: Literal["kimi", "openai_legacy", "_chaos"]
15
+ type: Literal["kimi", "openai_legacy", "openai_responses", "anthropic", "_chaos"]
16
16
  """Provider type"""
17
17
  base_url: str
18
18
  """API base URL"""
@@ -1,10 +1,7 @@
1
1
  import os
2
- from typing import NamedTuple
2
+ from typing import NamedTuple, cast, get_args
3
3
 
4
4
  from kosong.base.chat_provider import ChatProvider
5
- from kosong.chat_provider.chaos import ChaosChatProvider, ChaosConfig
6
- from kosong.chat_provider.kimi import Kimi
7
- from kosong.chat_provider.openai_legacy import OpenAILegacy
8
5
  from pydantic import SecretStr
9
6
 
10
7
  from kimi_cli.config import LLMModel, LLMModelCapability, LLMProvider
@@ -44,11 +41,19 @@ def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel) -> di
44
41
  applied["KIMI_API_KEY"] = "******"
45
42
  if model_name := os.getenv("KIMI_MODEL_NAME"):
46
43
  model.model = model_name
47
- applied["KIMI_MODEL_NAME"] = model.model
44
+ applied["KIMI_MODEL_NAME"] = model_name
48
45
  if max_context_size := os.getenv("KIMI_MODEL_MAX_CONTEXT_SIZE"):
49
46
  model.max_context_size = int(max_context_size)
50
- applied["KIMI_MODEL_MAX_CONTEXT_SIZE"] = str(model.max_context_size)
51
- case "openai_legacy":
47
+ applied["KIMI_MODEL_MAX_CONTEXT_SIZE"] = max_context_size
48
+ if capabilities := os.getenv("KIMI_MODEL_CAPABILITIES"):
49
+ caps_lower = (cap.strip().lower() for cap in capabilities.split(",") if cap.strip())
50
+ model.capabilities = set(
51
+ cast(LLMModelCapability, cap)
52
+ for cap in caps_lower
53
+ if cap in get_args(LLMModelCapability)
54
+ )
55
+ applied["KIMI_MODEL_CAPABILITIES"] = capabilities
56
+ case "openai_legacy" | "openai_responses":
52
57
  if base_url := os.getenv("OPENAI_BASE_URL"):
53
58
  provider.base_url = base_url
54
59
  if api_key := os.getenv("OPENAI_API_KEY"):
@@ -68,6 +73,8 @@ def create_llm(
68
73
  ) -> LLM:
69
74
  match provider.type:
70
75
  case "kimi":
76
+ from kosong.chat_provider.kimi import Kimi
77
+
71
78
  chat_provider = Kimi(
72
79
  model=model.model,
73
80
  base_url=provider.base_url,
@@ -81,13 +88,40 @@ def create_llm(
81
88
  if session_id:
82
89
  chat_provider = chat_provider.with_generation_kwargs(prompt_cache_key=session_id)
83
90
  case "openai_legacy":
91
+ from kosong.chat_provider.openai_legacy import OpenAILegacy
92
+
84
93
  chat_provider = OpenAILegacy(
85
94
  model=model.model,
86
95
  base_url=provider.base_url,
87
96
  api_key=provider.api_key.get_secret_value(),
88
97
  stream=stream,
89
98
  )
99
+ case "openai_responses":
100
+ from kosong.chat_provider.openai_responses import OpenAIResponses
101
+
102
+ chat_provider = OpenAIResponses(
103
+ model=model.model,
104
+ base_url=provider.base_url,
105
+ api_key=provider.api_key.get_secret_value(),
106
+ stream=stream,
107
+ )
108
+ case "anthropic":
109
+ from kosong.chat_provider.anthropic import Anthropic
110
+
111
+ chat_provider = Anthropic(
112
+ model=model.model,
113
+ base_url=provider.base_url,
114
+ api_key=provider.api_key.get_secret_value(),
115
+ stream=stream,
116
+ default_max_tokens=50000,
117
+ ).with_generation_kwargs(
118
+ # TODO: support configurable values
119
+ thinking={"type": "enabled", "budget_tokens": 1024},
120
+ beta_features=["interleaved-thinking-2025-05-14"],
121
+ )
90
122
  case "_chaos":
123
+ from kosong.chat_provider.chaos import ChaosChatProvider, ChaosConfig
124
+
91
125
  chat_provider = ChaosChatProvider(
92
126
  model=model.model,
93
127
  base_url=provider.base_url,
@@ -4,7 +4,6 @@ import string
4
4
  from pathlib import Path
5
5
  from typing import Any, NamedTuple
6
6
 
7
- import fastmcp
8
7
  from kosong.tooling import CallableTool, CallableTool2, Toolset
9
8
 
10
9
  from kimi_cli.agentspec import ResolvedAgentSpec, load_agent_spec
@@ -14,7 +13,7 @@ from kimi_cli.soul.approval import Approval
14
13
  from kimi_cli.soul.denwarenji import DenwaRenji
15
14
  from kimi_cli.soul.runtime import BuiltinSystemPromptArgs, Runtime
16
15
  from kimi_cli.soul.toolset import CustomToolset
17
- from kimi_cli.tools.mcp import MCPTool
16
+ from kimi_cli.tools import SkipThisTool
18
17
  from kimi_cli.utils.logging import logger
19
18
 
20
19
 
@@ -101,7 +100,11 @@ def _load_tools(
101
100
  ) -> list[str]:
102
101
  bad_tools: list[str] = []
103
102
  for tool_path in tool_paths:
104
- tool = _load_tool(tool_path, dependencies)
103
+ try:
104
+ tool = _load_tool(tool_path, dependencies)
105
+ except SkipThisTool:
106
+ logger.info("Skipping tool: {tool_path}", tool_path=tool_path)
107
+ continue
105
108
  if tool:
106
109
  toolset += tool
107
110
  else:
@@ -143,6 +146,10 @@ async def _load_mcp_tools(
143
146
  ValueError: If the MCP config is not valid.
144
147
  RuntimeError: If the MCP server cannot be connected.
145
148
  """
149
+ import fastmcp
150
+
151
+ from kimi_cli.tools.mcp import MCPTool
152
+
146
153
  for mcp_config in mcp_configs:
147
154
  logger.info("Loading MCP tools from: {mcp_config}", mcp_config=mcp_config)
148
155
  client = fastmcp.Client(mcp_config)
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import subprocess
2
3
  import sys
3
4
  from datetime import datetime
@@ -75,9 +76,10 @@ class Runtime(NamedTuple):
75
76
  session: Session,
76
77
  yolo: bool,
77
78
  ) -> "Runtime":
78
- # FIXME: do these asynchronously
79
- ls_output = _list_work_dir(session.work_dir)
80
- agents_md = load_agents_md(session.work_dir) or ""
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
+ )
81
83
 
82
84
  return Runtime(
83
85
  config=config,
@@ -87,7 +89,7 @@ class Runtime(NamedTuple):
87
89
  KIMI_NOW=datetime.now().astimezone().isoformat(),
88
90
  KIMI_WORK_DIR=session.work_dir,
89
91
  KIMI_WORK_DIR_LS=ls_output,
90
- KIMI_AGENTS_MD=agents_md,
92
+ KIMI_AGENTS_MD=agents_md or "",
91
93
  ),
92
94
  denwa_renji=DenwaRenji(),
93
95
  approval=Approval(yolo=yolo),
@@ -2,7 +2,8 @@ from contextvars import ContextVar
2
2
  from typing import override
3
3
 
4
4
  from kosong.base.message import ToolCall
5
- from kosong.tooling import HandleResult, SimpleToolset
5
+ from kosong.tooling import HandleResult
6
+ from kosong.tooling.simple import SimpleToolset
6
7
 
7
8
  current_tool_call = ContextVar[ToolCall | None]("current_tool_call", default=None)
8
9
 
@@ -1,12 +1,19 @@
1
1
  import json
2
2
  from pathlib import Path
3
+ from typing import cast
3
4
 
4
- import streamingjson
5
+ import streamingjson # pyright: ignore[reportMissingTypeStubs]
5
6
  from kosong.utils.typing import JsonType
6
7
 
7
8
  from kimi_cli.utils.string import shorten_middle
8
9
 
9
10
 
11
+ class SkipThisTool(Exception):
12
+ """Raised when a tool decides to skip itself from the loading process."""
13
+
14
+ pass
15
+
16
+
10
17
  def extract_subtitle(lexer: streamingjson.Lexer, tool_name: str) -> str | None:
11
18
  try:
12
19
  curr_args: JsonType = json.loads(lexer.complete_json())
@@ -29,15 +36,15 @@ def extract_subtitle(lexer: streamingjson.Lexer, tool_name: str) -> str | None:
29
36
  case "SetTodoList":
30
37
  if not isinstance(curr_args, dict) or not curr_args.get("todos"):
31
38
  return None
32
- if not isinstance(curr_args["todos"], list):
39
+
40
+ from kimi_cli.tools.todo import Params
41
+
42
+ try:
43
+ todo_params = Params.model_validate(curr_args)
44
+ for todo in todo_params.todos:
45
+ subtitle += f"• {todo.title} [{todo.status}]\n"
46
+ except Exception:
33
47
  return None
34
- for todo in curr_args["todos"]:
35
- if not isinstance(todo, dict) or not todo.get("title"):
36
- continue
37
- subtitle += f"• {todo['title']}"
38
- if todo.get("status"):
39
- subtitle += f" [{todo['status']}]"
40
- subtitle += "\n"
41
48
  return "\n" + subtitle.strip()
42
49
  case "Bash":
43
50
  if not isinstance(curr_args, dict) or not curr_args.get("command"):
@@ -72,7 +79,9 @@ def extract_subtitle(lexer: streamingjson.Lexer, tool_name: str) -> str | None:
72
79
  return None
73
80
  subtitle = str(curr_args["url"])
74
81
  case _:
75
- subtitle = "".join(lexer.json_content)
82
+ # lexer.json_content is list[str] based on streamingjson source code
83
+ content: list[str] = cast(list[str], lexer.json_content) # pyright: ignore[reportUnknownMemberType]
84
+ subtitle = "".join(content)
76
85
  if tool_name not in ["SetTodoList"]:
77
86
  subtitle = shorten_middle(subtitle, width=50)
78
87
  return subtitle
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
+ from collections.abc import Callable
2
3
  from pathlib import Path
3
- from typing import override
4
+ from typing import Any, override
4
5
 
5
6
  from kosong.tooling import CallableTool2, ToolReturnType
6
7
  from pydantic import BaseModel, Field
@@ -29,7 +30,7 @@ class Bash(CallableTool2[Params]):
29
30
  description: str = load_desc(Path(__file__).parent / "bash.md", {})
30
31
  params: type[Params] = Params
31
32
 
32
- def __init__(self, approval: Approval, **kwargs):
33
+ def __init__(self, approval: Approval, **kwargs: Any):
33
34
  super().__init__(**kwargs)
34
35
  self._approval = approval
35
36
 
@@ -71,8 +72,13 @@ class Bash(CallableTool2[Params]):
71
72
  )
72
73
 
73
74
 
74
- async def _stream_subprocess(command: str, stdout_cb, stderr_cb, timeout: int) -> int:
75
- async def _read_stream(stream, cb):
75
+ async def _stream_subprocess(
76
+ command: str,
77
+ stdout_cb: Callable[[bytes], None],
78
+ stderr_cb: Callable[[bytes], None],
79
+ timeout: int,
80
+ ) -> int:
81
+ async def _read_stream(stream: asyncio.StreamReader, cb: Callable[[bytes], None]):
76
82
  while True:
77
83
  line = await stream.readline()
78
84
  if line:
@@ -85,6 +91,9 @@ async def _stream_subprocess(command: str, stdout_cb, stderr_cb, timeout: int) -
85
91
  command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
86
92
  )
87
93
 
94
+ assert process.stdout is not None, "stdout is None"
95
+ assert process.stderr is not None, "stderr is None"
96
+
88
97
  try:
89
98
  await asyncio.wait_for(
90
99
  asyncio.gather(
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import override
2
+ from typing import Any, override
3
3
 
4
4
  from kosong.tooling import CallableTool2, ToolError, ToolReturnType
5
5
 
@@ -8,12 +8,12 @@ from kimi_cli.soul.denwarenji import DenwaRenji, DenwaRenjiError, DMail
8
8
  NAME = "SendDMail"
9
9
 
10
10
 
11
- class SendDMail(CallableTool2):
11
+ class SendDMail(CallableTool2[DMail]):
12
12
  name: str = NAME
13
13
  description: str = (Path(__file__).parent / "dmail.md").read_text(encoding="utf-8")
14
14
  params: type[DMail] = DMail
15
15
 
16
- def __init__(self, denwa_renji: DenwaRenji, **kwargs):
16
+ def __init__(self, denwa_renji: DenwaRenji, **kwargs: Any) -> None:
17
17
  super().__init__(**kwargs)
18
18
  self._denwa_renji = denwa_renji
19
19
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  from pathlib import Path
5
- from typing import override
5
+ from typing import Any, override
6
6
 
7
7
  import aiofiles.os
8
8
  from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnType
@@ -38,7 +38,7 @@ class Glob(CallableTool2[Params]):
38
38
  )
39
39
  params: type[Params] = Params
40
40
 
41
- def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs):
41
+ def __init__(self, builtin_args: BuiltinSystemPromptArgs, **kwargs: Any) -> None:
42
42
  super().__init__(**kwargs)
43
43
  self._work_dir = builtin_args.KIMI_WORK_DIR
44
44