kimi-cli 0.44__py3-none-any.whl → 0.78__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (137) hide show
  1. kimi_cli/CHANGELOG.md +349 -40
  2. kimi_cli/__init__.py +6 -0
  3. kimi_cli/acp/AGENTS.md +91 -0
  4. kimi_cli/acp/__init__.py +13 -0
  5. kimi_cli/acp/convert.py +111 -0
  6. kimi_cli/acp/kaos.py +270 -0
  7. kimi_cli/acp/mcp.py +46 -0
  8. kimi_cli/acp/server.py +335 -0
  9. kimi_cli/acp/session.py +445 -0
  10. kimi_cli/acp/tools.py +158 -0
  11. kimi_cli/acp/types.py +13 -0
  12. kimi_cli/agents/default/agent.yaml +4 -4
  13. kimi_cli/agents/default/sub.yaml +2 -1
  14. kimi_cli/agents/default/system.md +79 -21
  15. kimi_cli/agents/okabe/agent.yaml +17 -0
  16. kimi_cli/agentspec.py +53 -25
  17. kimi_cli/app.py +180 -52
  18. kimi_cli/cli/__init__.py +595 -0
  19. kimi_cli/cli/__main__.py +8 -0
  20. kimi_cli/cli/info.py +63 -0
  21. kimi_cli/cli/mcp.py +349 -0
  22. kimi_cli/config.py +153 -17
  23. kimi_cli/constant.py +3 -0
  24. kimi_cli/exception.py +23 -2
  25. kimi_cli/flow/__init__.py +117 -0
  26. kimi_cli/flow/d2.py +376 -0
  27. kimi_cli/flow/mermaid.py +218 -0
  28. kimi_cli/llm.py +129 -23
  29. kimi_cli/metadata.py +32 -7
  30. kimi_cli/platforms.py +262 -0
  31. kimi_cli/prompts/__init__.py +2 -0
  32. kimi_cli/prompts/compact.md +4 -5
  33. kimi_cli/session.py +223 -31
  34. kimi_cli/share.py +2 -0
  35. kimi_cli/skill.py +145 -0
  36. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  37. kimi_cli/skills/skill-creator/SKILL.md +351 -0
  38. kimi_cli/soul/__init__.py +51 -20
  39. kimi_cli/soul/agent.py +213 -85
  40. kimi_cli/soul/approval.py +86 -17
  41. kimi_cli/soul/compaction.py +64 -53
  42. kimi_cli/soul/context.py +38 -5
  43. kimi_cli/soul/denwarenji.py +2 -0
  44. kimi_cli/soul/kimisoul.py +442 -60
  45. kimi_cli/soul/message.py +54 -54
  46. kimi_cli/soul/slash.py +72 -0
  47. kimi_cli/soul/toolset.py +387 -6
  48. kimi_cli/toad.py +74 -0
  49. kimi_cli/tools/AGENTS.md +5 -0
  50. kimi_cli/tools/__init__.py +42 -34
  51. kimi_cli/tools/display.py +25 -0
  52. kimi_cli/tools/dmail/__init__.py +10 -10
  53. kimi_cli/tools/dmail/dmail.md +11 -9
  54. kimi_cli/tools/file/__init__.py +1 -3
  55. kimi_cli/tools/file/glob.py +20 -23
  56. kimi_cli/tools/file/grep.md +1 -1
  57. kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
  58. kimi_cli/tools/file/read.md +24 -6
  59. kimi_cli/tools/file/read.py +134 -50
  60. kimi_cli/tools/file/replace.md +1 -1
  61. kimi_cli/tools/file/replace.py +36 -29
  62. kimi_cli/tools/file/utils.py +282 -0
  63. kimi_cli/tools/file/write.py +43 -22
  64. kimi_cli/tools/multiagent/__init__.py +7 -0
  65. kimi_cli/tools/multiagent/create.md +11 -0
  66. kimi_cli/tools/multiagent/create.py +50 -0
  67. kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
  68. kimi_cli/tools/shell/__init__.py +120 -0
  69. kimi_cli/tools/{bash → shell}/bash.md +1 -2
  70. kimi_cli/tools/shell/powershell.md +25 -0
  71. kimi_cli/tools/test.py +4 -4
  72. kimi_cli/tools/think/__init__.py +2 -2
  73. kimi_cli/tools/todo/__init__.py +14 -8
  74. kimi_cli/tools/utils.py +64 -24
  75. kimi_cli/tools/web/fetch.py +68 -13
  76. kimi_cli/tools/web/search.py +10 -12
  77. kimi_cli/ui/acp/__init__.py +65 -412
  78. kimi_cli/ui/print/__init__.py +37 -49
  79. kimi_cli/ui/print/visualize.py +179 -0
  80. kimi_cli/ui/shell/__init__.py +141 -84
  81. kimi_cli/ui/shell/console.py +2 -0
  82. kimi_cli/ui/shell/debug.py +28 -23
  83. kimi_cli/ui/shell/keyboard.py +5 -1
  84. kimi_cli/ui/shell/prompt.py +220 -194
  85. kimi_cli/ui/shell/replay.py +111 -46
  86. kimi_cli/ui/shell/setup.py +89 -82
  87. kimi_cli/ui/shell/slash.py +422 -0
  88. kimi_cli/ui/shell/update.py +4 -2
  89. kimi_cli/ui/shell/usage.py +271 -0
  90. kimi_cli/ui/shell/visualize.py +574 -72
  91. kimi_cli/ui/wire/__init__.py +267 -0
  92. kimi_cli/ui/wire/jsonrpc.py +142 -0
  93. kimi_cli/ui/wire/protocol.py +1 -0
  94. kimi_cli/utils/__init__.py +0 -0
  95. kimi_cli/utils/aiohttp.py +2 -0
  96. kimi_cli/utils/aioqueue.py +72 -0
  97. kimi_cli/utils/broadcast.py +37 -0
  98. kimi_cli/utils/changelog.py +12 -7
  99. kimi_cli/utils/clipboard.py +12 -0
  100. kimi_cli/utils/datetime.py +37 -0
  101. kimi_cli/utils/environment.py +58 -0
  102. kimi_cli/utils/envvar.py +12 -0
  103. kimi_cli/utils/frontmatter.py +44 -0
  104. kimi_cli/utils/logging.py +7 -6
  105. kimi_cli/utils/message.py +9 -14
  106. kimi_cli/utils/path.py +99 -9
  107. kimi_cli/utils/pyinstaller.py +6 -0
  108. kimi_cli/utils/rich/__init__.py +33 -0
  109. kimi_cli/utils/rich/columns.py +99 -0
  110. kimi_cli/utils/rich/markdown.py +961 -0
  111. kimi_cli/utils/rich/markdown_sample.md +108 -0
  112. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  113. kimi_cli/utils/signals.py +2 -0
  114. kimi_cli/utils/slashcmd.py +124 -0
  115. kimi_cli/utils/string.py +2 -0
  116. kimi_cli/utils/term.py +168 -0
  117. kimi_cli/utils/typing.py +20 -0
  118. kimi_cli/wire/__init__.py +98 -29
  119. kimi_cli/wire/serde.py +45 -0
  120. kimi_cli/wire/types.py +299 -0
  121. kimi_cli-0.78.dist-info/METADATA +200 -0
  122. kimi_cli-0.78.dist-info/RECORD +135 -0
  123. kimi_cli-0.78.dist-info/entry_points.txt +4 -0
  124. kimi_cli/cli.py +0 -250
  125. kimi_cli/soul/runtime.py +0 -96
  126. kimi_cli/tools/bash/__init__.py +0 -99
  127. kimi_cli/tools/file/patch.md +0 -8
  128. kimi_cli/tools/file/patch.py +0 -143
  129. kimi_cli/tools/mcp.py +0 -85
  130. kimi_cli/ui/shell/liveview.py +0 -386
  131. kimi_cli/ui/shell/metacmd.py +0 -262
  132. kimi_cli/wire/message.py +0 -91
  133. kimi_cli-0.44.dist-info/METADATA +0 -188
  134. kimi_cli-0.44.dist-info/RECORD +0 -89
  135. kimi_cli-0.44.dist-info/entry_points.txt +0 -3
  136. /kimi_cli/tools/{task → multiagent}/task.md +0 -0
  137. {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
kimi_cli/acp/server.py ADDED
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import sys
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, NamedTuple
8
+
9
+ import acp
10
+ from kaos.path import KaosPath
11
+
12
+ from kimi_cli.acp.kaos import ACPKaos
13
+ from kimi_cli.acp.mcp import acp_mcp_servers_to_mcp_config
14
+ from kimi_cli.acp.session import ACPSession
15
+ from kimi_cli.acp.tools import replace_tools
16
+ from kimi_cli.acp.types import ACPContentBlock, MCPServer
17
+ from kimi_cli.app import KimiCLI
18
+ from kimi_cli.config import LLMModel, load_config, save_config
19
+ from kimi_cli.constant import NAME, VERSION
20
+ from kimi_cli.llm import create_llm, derive_model_capabilities
21
+ from kimi_cli.session import Session
22
+ from kimi_cli.soul.slash import registry as soul_slash_registry
23
+ from kimi_cli.soul.toolset import KimiToolset
24
+ from kimi_cli.utils.logging import logger
25
+
26
+
27
+ class ACPServer:
28
+ def __init__(self) -> None:
29
+ self.client_capabilities: acp.schema.ClientCapabilities | None = None
30
+ self.conn: acp.Client | None = None
31
+ self.sessions: dict[str, tuple[ACPSession, _ModelIDConv]] = {}
32
+
33
+ def on_connect(self, conn: acp.Client) -> None:
34
+ logger.info("ACP client connected")
35
+ self.conn = conn
36
+
37
+ async def initialize(
38
+ self,
39
+ protocol_version: int,
40
+ client_capabilities: acp.schema.ClientCapabilities | None = None,
41
+ client_info: acp.schema.Implementation | None = None,
42
+ **kwargs: Any,
43
+ ) -> acp.InitializeResponse:
44
+ logger.info(
45
+ "ACP server initialized with protocol version: {version}, "
46
+ "client capabilities: {capabilities}, client info: {info}",
47
+ version=protocol_version,
48
+ capabilities=client_capabilities,
49
+ info=client_info,
50
+ )
51
+ self.client_capabilities = client_capabilities
52
+
53
+ # get command and args of current process for terminal-auth
54
+ command = sys.argv[0]
55
+ if command.endswith("kimi"):
56
+ args = []
57
+ else:
58
+ idx = sys.argv.index("kimi")
59
+ args = sys.argv[1 : idx + 1]
60
+
61
+ return acp.InitializeResponse(
62
+ protocol_version=protocol_version,
63
+ agent_capabilities=acp.schema.AgentCapabilities(
64
+ load_session=True,
65
+ prompt_capabilities=acp.schema.PromptCapabilities(
66
+ embedded_context=False, image=True, audio=False
67
+ ),
68
+ mcp_capabilities=acp.schema.McpCapabilities(http=True, sse=False),
69
+ session_capabilities=acp.schema.SessionCapabilities(
70
+ list=acp.schema.SessionListCapabilities(),
71
+ ),
72
+ ),
73
+ auth_methods=[
74
+ acp.schema.AuthMethod(
75
+ id="setup",
76
+ name="Setup LLM with /setup slash command",
77
+ description=(
78
+ "Run `kimi` command in the terminal, "
79
+ "then send `/setup` command to complete the setup."
80
+ ),
81
+ field_meta={
82
+ "terminal-auth": {
83
+ "command": command,
84
+ "args": args,
85
+ "label": "Kimi CLI Setup",
86
+ }
87
+ },
88
+ )
89
+ ],
90
+ agent_info=acp.schema.Implementation(name=NAME, version=VERSION),
91
+ )
92
+
93
+ async def new_session(
94
+ self, cwd: str, mcp_servers: list[MCPServer], **kwargs: Any
95
+ ) -> acp.NewSessionResponse:
96
+ logger.info("Creating new session for working directory: {cwd}", cwd=cwd)
97
+ assert self.conn is not None, "ACP client not connected"
98
+ assert self.client_capabilities is not None, "ACP connection not initialized"
99
+
100
+ session = await Session.create(KaosPath.unsafe_from_local_path(Path(cwd)))
101
+
102
+ mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers)
103
+ cli_instance = await KimiCLI.create(
104
+ session,
105
+ mcp_configs=[mcp_config],
106
+ )
107
+ config = cli_instance.soul.runtime.config
108
+ acp_kaos = ACPKaos(self.conn, session.id, self.client_capabilities)
109
+ acp_session = ACPSession(session.id, cli_instance, self.conn, kaos=acp_kaos)
110
+ model_id_conv = _ModelIDConv(config.default_model, config.default_thinking)
111
+ self.sessions[session.id] = (acp_session, model_id_conv)
112
+
113
+ if isinstance(cli_instance.soul.agent.toolset, KimiToolset):
114
+ replace_tools(
115
+ self.client_capabilities,
116
+ self.conn,
117
+ session.id,
118
+ cli_instance.soul.agent.toolset,
119
+ cli_instance.soul.runtime,
120
+ )
121
+
122
+ available_commands = [
123
+ acp.schema.AvailableCommand(name=cmd.name, description=cmd.description)
124
+ for cmd in soul_slash_registry.list_commands()
125
+ ]
126
+ asyncio.create_task(
127
+ self.conn.session_update(
128
+ session_id=session.id,
129
+ update=acp.schema.AvailableCommandsUpdate(
130
+ session_update="available_commands_update",
131
+ available_commands=available_commands,
132
+ ),
133
+ )
134
+ )
135
+ return acp.NewSessionResponse(
136
+ session_id=session.id,
137
+ modes=acp.schema.SessionModeState(
138
+ available_modes=[
139
+ acp.schema.SessionMode(
140
+ id="default",
141
+ name="Default",
142
+ description="The default mode.",
143
+ ),
144
+ ],
145
+ current_mode_id="default",
146
+ ),
147
+ models=acp.schema.SessionModelState(
148
+ available_models=_expand_llm_models(config.models),
149
+ current_model_id=model_id_conv.to_acp_model_id(),
150
+ ),
151
+ )
152
+
153
+ async def load_session(
154
+ self, cwd: str, mcp_servers: list[MCPServer], session_id: str, **kwargs: Any
155
+ ) -> None:
156
+ logger.info("Loading session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)
157
+ assert self.conn is not None, "ACP client not connected"
158
+ assert self.client_capabilities is not None, "ACP connection not initialized"
159
+
160
+ if session_id in self.sessions:
161
+ logger.warning("Session already loaded: {id}", id=session_id)
162
+ return
163
+
164
+ work_dir = KaosPath.unsafe_from_local_path(Path(cwd))
165
+ session = await Session.find(work_dir, session_id)
166
+ if session is None:
167
+ logger.error(
168
+ "Session not found: {id} for working directory: {cwd}", id=session_id, cwd=cwd
169
+ )
170
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
171
+
172
+ mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers)
173
+ cli_instance = await KimiCLI.create(
174
+ session,
175
+ mcp_configs=[mcp_config],
176
+ )
177
+ config = cli_instance.soul.runtime.config
178
+ acp_kaos = ACPKaos(self.conn, session.id, self.client_capabilities)
179
+ acp_session = ACPSession(session.id, cli_instance, self.conn, kaos=acp_kaos)
180
+ model_id_conv = _ModelIDConv(config.default_model, config.default_thinking)
181
+ self.sessions[session.id] = (acp_session, model_id_conv)
182
+
183
+ if isinstance(cli_instance.soul.agent.toolset, KimiToolset):
184
+ replace_tools(
185
+ self.client_capabilities,
186
+ self.conn,
187
+ session.id,
188
+ cli_instance.soul.agent.toolset,
189
+ cli_instance.soul.runtime,
190
+ )
191
+
192
+ # TODO: replay session history?
193
+
194
+ async def list_sessions(
195
+ self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
196
+ ) -> acp.schema.ListSessionsResponse:
197
+ logger.info("Listing sessions for working directory: {cwd}", cwd=cwd)
198
+ if cwd is None:
199
+ return acp.schema.ListSessionsResponse(sessions=[], next_cursor=None)
200
+ work_dir = KaosPath.unsafe_from_local_path(Path(cwd))
201
+ sessions = await Session.list(work_dir)
202
+ return acp.schema.ListSessionsResponse(
203
+ sessions=[
204
+ acp.schema.SessionInfo(
205
+ cwd=cwd,
206
+ session_id=s.id,
207
+ title=s.title,
208
+ updated_at=datetime.fromtimestamp(s.updated_at).astimezone().isoformat(),
209
+ )
210
+ for s in sessions
211
+ ],
212
+ next_cursor=None,
213
+ )
214
+
215
+ async def set_session_mode(self, mode_id: str, session_id: str, **kwargs: Any) -> None:
216
+ assert mode_id == "default", "Only default mode is supported"
217
+
218
+ async def set_session_model(self, model_id: str, session_id: str, **kwargs: Any) -> None:
219
+ logger.info(
220
+ "Setting session model to {model_id} for session: {id}",
221
+ model_id=model_id,
222
+ id=session_id,
223
+ )
224
+ if session_id not in self.sessions:
225
+ logger.error("Session not found: {id}", id=session_id)
226
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
227
+
228
+ acp_session, current_model_id = self.sessions[session_id]
229
+ cli_instance = acp_session.cli
230
+ model_id_conv = _ModelIDConv.from_acp_model_id(model_id)
231
+ if model_id_conv == current_model_id:
232
+ return
233
+
234
+ config = cli_instance.soul.runtime.config
235
+ new_model = config.models.get(model_id_conv.model_key)
236
+ if new_model is None:
237
+ logger.error("Model not found: {model_key}", model_key=model_id_conv.model_key)
238
+ raise acp.RequestError.invalid_params({"model_id": "Model not found"})
239
+ new_provider = config.providers.get(new_model.provider)
240
+ if new_provider is None:
241
+ logger.error(
242
+ "Provider not found: {provider} for model: {model_key}",
243
+ provider=new_model.provider,
244
+ model_key=model_id_conv.model_key,
245
+ )
246
+ raise acp.RequestError.invalid_params({"model_id": "Model's provider not found"})
247
+
248
+ new_llm = create_llm(
249
+ new_provider,
250
+ new_model,
251
+ session_id=acp_session.id,
252
+ thinking=model_id_conv.thinking,
253
+ )
254
+ cli_instance.soul.runtime.llm = new_llm
255
+
256
+ config.default_model = model_id_conv.model_key
257
+ config.default_thinking = model_id_conv.thinking
258
+ assert config.is_from_default_location, "`kimi acp` must use the default config location"
259
+ config_for_save = load_config()
260
+ config_for_save.default_model = model_id_conv.model_key
261
+ config_for_save.default_thinking = model_id_conv.thinking
262
+ save_config(config_for_save)
263
+
264
+ async def authenticate(self, method_id: str, **kwargs: Any) -> acp.AuthenticateResponse | None:
265
+ raise NotImplementedError
266
+
267
+ async def prompt(
268
+ self, prompt: list[ACPContentBlock], session_id: str, **kwargs: Any
269
+ ) -> acp.PromptResponse:
270
+ logger.info("Received prompt request for session: {id}", id=session_id)
271
+ if session_id not in self.sessions:
272
+ logger.error("Session not found: {id}", id=session_id)
273
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
274
+ acp_session, *_ = self.sessions[session_id]
275
+ return await acp_session.prompt(prompt)
276
+
277
+ async def cancel(self, session_id: str, **kwargs: Any) -> None:
278
+ logger.info("Received cancel request for session: {id}", id=session_id)
279
+ if session_id not in self.sessions:
280
+ logger.error("Session not found: {id}", id=session_id)
281
+ raise acp.RequestError.invalid_params({"session_id": "Session not found"})
282
+ acp_session, *_ = self.sessions[session_id]
283
+ await acp_session.cancel()
284
+
285
+ async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
286
+ raise NotImplementedError
287
+
288
+ async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
289
+ raise NotImplementedError
290
+
291
+
292
+ class _ModelIDConv(NamedTuple):
293
+ model_key: str
294
+ thinking: bool
295
+
296
+ @classmethod
297
+ def from_acp_model_id(cls, model_id: str) -> _ModelIDConv:
298
+ if model_id.endswith(",thinking"):
299
+ return _ModelIDConv(model_id[: -len(",thinking")], True)
300
+ return _ModelIDConv(model_id, False)
301
+
302
+ def to_acp_model_id(self) -> str:
303
+ if self.thinking:
304
+ return f"{self.model_key},thinking"
305
+ return self.model_key
306
+
307
+
308
+ def _expand_llm_models(models: dict[str, LLMModel]) -> list[acp.schema.ModelInfo]:
309
+ expanded_models: list[acp.schema.ModelInfo] = []
310
+ for model_key, model in models.items():
311
+ capabilities = derive_model_capabilities(model)
312
+ if "thinking" in model.model or "reason" in model.model:
313
+ # always-thinking models
314
+ expanded_models.append(
315
+ acp.schema.ModelInfo(
316
+ model_id=_ModelIDConv(model_key, True).to_acp_model_id(),
317
+ name=f"{model.model}",
318
+ )
319
+ )
320
+ else:
321
+ expanded_models.append(
322
+ acp.schema.ModelInfo(
323
+ model_id=model_key,
324
+ name=model.model,
325
+ )
326
+ )
327
+ if "thinking" in capabilities:
328
+ # add thinking variant
329
+ expanded_models.append(
330
+ acp.schema.ModelInfo(
331
+ model_id=_ModelIDConv(model_key, True).to_acp_model_id(),
332
+ name=f"{model.model} (thinking)",
333
+ )
334
+ )
335
+ return expanded_models