bhu-cli 1.46.0__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 (258) hide show
  1. bhu_cli-1.46.0.dist-info/METADATA +210 -0
  2. bhu_cli-1.46.0.dist-info/RECORD +258 -0
  3. bhu_cli-1.46.0.dist-info/WHEEL +4 -0
  4. bhu_cli-1.46.0.dist-info/entry_points.txt +4 -0
  5. kimi_cli/CHANGELOG.md +1281 -0
  6. kimi_cli/__init__.py +29 -0
  7. kimi_cli/__main__.py +43 -0
  8. kimi_cli/_build_info.py +1 -0
  9. kimi_cli/acp/AGENTS.md +92 -0
  10. kimi_cli/acp/__init__.py +13 -0
  11. kimi_cli/acp/convert.py +128 -0
  12. kimi_cli/acp/kaos.py +291 -0
  13. kimi_cli/acp/mcp.py +46 -0
  14. kimi_cli/acp/server.py +468 -0
  15. kimi_cli/acp/session.py +499 -0
  16. kimi_cli/acp/tools.py +167 -0
  17. kimi_cli/acp/types.py +13 -0
  18. kimi_cli/acp/version.py +45 -0
  19. kimi_cli/agents/default/agent.yaml +36 -0
  20. kimi_cli/agents/default/coder.yaml +25 -0
  21. kimi_cli/agents/default/explore.yaml +46 -0
  22. kimi_cli/agents/default/plan.yaml +30 -0
  23. kimi_cli/agents/default/system.md +160 -0
  24. kimi_cli/agents/okabe/agent.yaml +22 -0
  25. kimi_cli/agentspec.py +160 -0
  26. kimi_cli/app.py +807 -0
  27. kimi_cli/approval_runtime/__init__.py +29 -0
  28. kimi_cli/approval_runtime/models.py +42 -0
  29. kimi_cli/approval_runtime/runtime.py +235 -0
  30. kimi_cli/auth/__init__.py +5 -0
  31. kimi_cli/auth/oauth.py +1092 -0
  32. kimi_cli/auth/platforms.py +374 -0
  33. kimi_cli/background/__init__.py +36 -0
  34. kimi_cli/background/agent_runner.py +231 -0
  35. kimi_cli/background/ids.py +19 -0
  36. kimi_cli/background/manager.py +725 -0
  37. kimi_cli/background/models.py +105 -0
  38. kimi_cli/background/store.py +237 -0
  39. kimi_cli/background/summary.py +66 -0
  40. kimi_cli/background/worker.py +209 -0
  41. kimi_cli/cli/__init__.py +1081 -0
  42. kimi_cli/cli/__main__.py +34 -0
  43. kimi_cli/cli/_lazy_group.py +238 -0
  44. kimi_cli/cli/export.py +322 -0
  45. kimi_cli/cli/info.py +62 -0
  46. kimi_cli/cli/mcp.py +353 -0
  47. kimi_cli/cli/plugin.py +347 -0
  48. kimi_cli/cli/toad.py +73 -0
  49. kimi_cli/cli/vis.py +38 -0
  50. kimi_cli/cli/web.py +80 -0
  51. kimi_cli/config.py +429 -0
  52. kimi_cli/constant.py +116 -0
  53. kimi_cli/exception.py +43 -0
  54. kimi_cli/hooks/__init__.py +24 -0
  55. kimi_cli/hooks/config.py +34 -0
  56. kimi_cli/hooks/defaults/rtk.py +48 -0
  57. kimi_cli/hooks/engine.py +394 -0
  58. kimi_cli/hooks/events.py +190 -0
  59. kimi_cli/hooks/runner.py +103 -0
  60. kimi_cli/llm.py +333 -0
  61. kimi_cli/mcp_oauth.py +72 -0
  62. kimi_cli/metadata.py +79 -0
  63. kimi_cli/notifications/__init__.py +33 -0
  64. kimi_cli/notifications/llm.py +77 -0
  65. kimi_cli/notifications/manager.py +145 -0
  66. kimi_cli/notifications/models.py +50 -0
  67. kimi_cli/notifications/notifier.py +41 -0
  68. kimi_cli/notifications/store.py +118 -0
  69. kimi_cli/notifications/wire.py +21 -0
  70. kimi_cli/plugin/__init__.py +124 -0
  71. kimi_cli/plugin/manager.py +153 -0
  72. kimi_cli/plugin/tool.py +173 -0
  73. kimi_cli/prompts/__init__.py +6 -0
  74. kimi_cli/prompts/compact.md +73 -0
  75. kimi_cli/prompts/init.md +21 -0
  76. kimi_cli/py.typed +0 -0
  77. kimi_cli/session.py +319 -0
  78. kimi_cli/session_fork.py +325 -0
  79. kimi_cli/session_state.py +132 -0
  80. kimi_cli/share.py +14 -0
  81. kimi_cli/skill/__init__.py +727 -0
  82. kimi_cli/skill/flow/__init__.py +99 -0
  83. kimi_cli/skill/flow/d2.py +482 -0
  84. kimi_cli/skill/flow/mermaid.py +266 -0
  85. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  86. kimi_cli/skills/skill-creator/SKILL.md +367 -0
  87. kimi_cli/soul/__init__.py +304 -0
  88. kimi_cli/soul/agent.py +519 -0
  89. kimi_cli/soul/approval.py +267 -0
  90. kimi_cli/soul/btw.py +237 -0
  91. kimi_cli/soul/compaction.py +189 -0
  92. kimi_cli/soul/context.py +339 -0
  93. kimi_cli/soul/denwarenji.py +39 -0
  94. kimi_cli/soul/dynamic_injection.py +84 -0
  95. kimi_cli/soul/dynamic_injections/__init__.py +0 -0
  96. kimi_cli/soul/dynamic_injections/afk_mode.py +74 -0
  97. kimi_cli/soul/dynamic_injections/plan_mode.py +245 -0
  98. kimi_cli/soul/kimisoul.py +1710 -0
  99. kimi_cli/soul/message.py +92 -0
  100. kimi_cli/soul/slash.py +341 -0
  101. kimi_cli/soul/toolset.py +891 -0
  102. kimi_cli/subagents/__init__.py +21 -0
  103. kimi_cli/subagents/builder.py +42 -0
  104. kimi_cli/subagents/core.py +86 -0
  105. kimi_cli/subagents/git_context.py +170 -0
  106. kimi_cli/subagents/models.py +54 -0
  107. kimi_cli/subagents/output.py +71 -0
  108. kimi_cli/subagents/registry.py +28 -0
  109. kimi_cli/subagents/runner.py +428 -0
  110. kimi_cli/subagents/store.py +196 -0
  111. kimi_cli/telemetry/__init__.py +197 -0
  112. kimi_cli/telemetry/crash.py +149 -0
  113. kimi_cli/telemetry/sink.py +143 -0
  114. kimi_cli/telemetry/transport.py +318 -0
  115. kimi_cli/tools/AGENTS.md +5 -0
  116. kimi_cli/tools/__init__.py +105 -0
  117. kimi_cli/tools/agent/__init__.py +277 -0
  118. kimi_cli/tools/agent/description.md +41 -0
  119. kimi_cli/tools/ask_user/__init__.py +154 -0
  120. kimi_cli/tools/ask_user/description.md +19 -0
  121. kimi_cli/tools/background/__init__.py +318 -0
  122. kimi_cli/tools/background/list.md +10 -0
  123. kimi_cli/tools/background/output.md +11 -0
  124. kimi_cli/tools/background/stop.md +8 -0
  125. kimi_cli/tools/display.py +46 -0
  126. kimi_cli/tools/dmail/__init__.py +38 -0
  127. kimi_cli/tools/dmail/dmail.md +17 -0
  128. kimi_cli/tools/file/__init__.py +30 -0
  129. kimi_cli/tools/file/glob.md +21 -0
  130. kimi_cli/tools/file/glob.py +178 -0
  131. kimi_cli/tools/file/grep.md +6 -0
  132. kimi_cli/tools/file/grep_local.py +590 -0
  133. kimi_cli/tools/file/plan_mode.py +45 -0
  134. kimi_cli/tools/file/read.md +16 -0
  135. kimi_cli/tools/file/read.py +300 -0
  136. kimi_cli/tools/file/read_media.md +24 -0
  137. kimi_cli/tools/file/read_media.py +217 -0
  138. kimi_cli/tools/file/replace.md +7 -0
  139. kimi_cli/tools/file/replace.py +195 -0
  140. kimi_cli/tools/file/utils.py +257 -0
  141. kimi_cli/tools/file/write.md +5 -0
  142. kimi_cli/tools/file/write.py +177 -0
  143. kimi_cli/tools/plan/__init__.py +339 -0
  144. kimi_cli/tools/plan/description.md +29 -0
  145. kimi_cli/tools/plan/enter.py +197 -0
  146. kimi_cli/tools/plan/enter_description.md +35 -0
  147. kimi_cli/tools/plan/heroes.py +277 -0
  148. kimi_cli/tools/shell/__init__.py +260 -0
  149. kimi_cli/tools/shell/bash.md +35 -0
  150. kimi_cli/tools/test.py +55 -0
  151. kimi_cli/tools/think/__init__.py +21 -0
  152. kimi_cli/tools/think/think.md +1 -0
  153. kimi_cli/tools/todo/__init__.py +168 -0
  154. kimi_cli/tools/todo/set_todo_list.md +23 -0
  155. kimi_cli/tools/utils.py +199 -0
  156. kimi_cli/tools/web/__init__.py +4 -0
  157. kimi_cli/tools/web/fetch.md +1 -0
  158. kimi_cli/tools/web/fetch.py +189 -0
  159. kimi_cli/tools/web/search.md +1 -0
  160. kimi_cli/tools/web/search.py +163 -0
  161. kimi_cli/ui/__init__.py +0 -0
  162. kimi_cli/ui/acp/__init__.py +99 -0
  163. kimi_cli/ui/print/__init__.py +474 -0
  164. kimi_cli/ui/print/visualize.py +194 -0
  165. kimi_cli/ui/shell/__init__.py +1540 -0
  166. kimi_cli/ui/shell/console.py +109 -0
  167. kimi_cli/ui/shell/debug.py +190 -0
  168. kimi_cli/ui/shell/echo.py +17 -0
  169. kimi_cli/ui/shell/export_import.py +117 -0
  170. kimi_cli/ui/shell/keyboard.py +300 -0
  171. kimi_cli/ui/shell/mcp_status.py +111 -0
  172. kimi_cli/ui/shell/oauth.py +149 -0
  173. kimi_cli/ui/shell/placeholders.py +531 -0
  174. kimi_cli/ui/shell/prompt.py +2259 -0
  175. kimi_cli/ui/shell/replay.py +215 -0
  176. kimi_cli/ui/shell/session_picker.py +227 -0
  177. kimi_cli/ui/shell/setup.py +212 -0
  178. kimi_cli/ui/shell/slash.py +893 -0
  179. kimi_cli/ui/shell/startup.py +32 -0
  180. kimi_cli/ui/shell/task_browser.py +486 -0
  181. kimi_cli/ui/shell/update.py +349 -0
  182. kimi_cli/ui/shell/usage.py +295 -0
  183. kimi_cli/ui/shell/visualize/__init__.py +165 -0
  184. kimi_cli/ui/shell/visualize/_approval_panel.py +505 -0
  185. kimi_cli/ui/shell/visualize/_blocks.py +640 -0
  186. kimi_cli/ui/shell/visualize/_btw_panel.py +224 -0
  187. kimi_cli/ui/shell/visualize/_input_router.py +48 -0
  188. kimi_cli/ui/shell/visualize/_interactive.py +524 -0
  189. kimi_cli/ui/shell/visualize/_live_view.py +902 -0
  190. kimi_cli/ui/shell/visualize/_question_panel.py +586 -0
  191. kimi_cli/ui/theme.py +241 -0
  192. kimi_cli/utils/__init__.py +0 -0
  193. kimi_cli/utils/aiohttp.py +24 -0
  194. kimi_cli/utils/aioqueue.py +72 -0
  195. kimi_cli/utils/broadcast.py +37 -0
  196. kimi_cli/utils/changelog.py +108 -0
  197. kimi_cli/utils/clipboard.py +246 -0
  198. kimi_cli/utils/datetime.py +64 -0
  199. kimi_cli/utils/diff.py +135 -0
  200. kimi_cli/utils/editor.py +91 -0
  201. kimi_cli/utils/environment.py +225 -0
  202. kimi_cli/utils/envvar.py +22 -0
  203. kimi_cli/utils/export.py +696 -0
  204. kimi_cli/utils/file_filter.py +367 -0
  205. kimi_cli/utils/frontmatter.py +70 -0
  206. kimi_cli/utils/io.py +27 -0
  207. kimi_cli/utils/logging.py +124 -0
  208. kimi_cli/utils/media_tags.py +29 -0
  209. kimi_cli/utils/message.py +24 -0
  210. kimi_cli/utils/path.py +245 -0
  211. kimi_cli/utils/proctitle.py +33 -0
  212. kimi_cli/utils/proxy.py +31 -0
  213. kimi_cli/utils/pyinstaller.py +43 -0
  214. kimi_cli/utils/rich/__init__.py +33 -0
  215. kimi_cli/utils/rich/columns.py +99 -0
  216. kimi_cli/utils/rich/diff_render.py +481 -0
  217. kimi_cli/utils/rich/markdown.py +898 -0
  218. kimi_cli/utils/rich/markdown_sample.md +108 -0
  219. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  220. kimi_cli/utils/rich/syntax.py +114 -0
  221. kimi_cli/utils/sensitive.py +54 -0
  222. kimi_cli/utils/server.py +121 -0
  223. kimi_cli/utils/shell_quoting.py +38 -0
  224. kimi_cli/utils/signals.py +43 -0
  225. kimi_cli/utils/slashcmd.py +145 -0
  226. kimi_cli/utils/string.py +41 -0
  227. kimi_cli/utils/subprocess_env.py +73 -0
  228. kimi_cli/utils/term.py +168 -0
  229. kimi_cli/utils/typing.py +20 -0
  230. kimi_cli/utils/windows_paths.py +48 -0
  231. kimi_cli/vis/__init__.py +0 -0
  232. kimi_cli/vis/api/__init__.py +5 -0
  233. kimi_cli/vis/api/sessions.py +687 -0
  234. kimi_cli/vis/api/statistics.py +209 -0
  235. kimi_cli/vis/api/system.py +19 -0
  236. kimi_cli/vis/app.py +175 -0
  237. kimi_cli/web/__init__.py +5 -0
  238. kimi_cli/web/api/__init__.py +15 -0
  239. kimi_cli/web/api/config.py +208 -0
  240. kimi_cli/web/api/open_in.py +197 -0
  241. kimi_cli/web/api/sessions.py +1223 -0
  242. kimi_cli/web/app.py +451 -0
  243. kimi_cli/web/auth.py +191 -0
  244. kimi_cli/web/models.py +98 -0
  245. kimi_cli/web/runner/__init__.py +5 -0
  246. kimi_cli/web/runner/messages.py +57 -0
  247. kimi_cli/web/runner/process.py +754 -0
  248. kimi_cli/web/runner/worker.py +95 -0
  249. kimi_cli/web/store/__init__.py +1 -0
  250. kimi_cli/web/store/sessions.py +432 -0
  251. kimi_cli/wire/__init__.py +148 -0
  252. kimi_cli/wire/file.py +151 -0
  253. kimi_cli/wire/jsonrpc.py +263 -0
  254. kimi_cli/wire/protocol.py +2 -0
  255. kimi_cli/wire/root_hub.py +27 -0
  256. kimi_cli/wire/serde.py +26 -0
  257. kimi_cli/wire/server.py +1060 -0
  258. kimi_cli/wire/types.py +717 -0
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from kosong.message import Message
6
+ from kosong.tooling.error import ToolRuntimeError
7
+
8
+ from kimi_cli.llm import ModelCapability
9
+ from kimi_cli.wire.types import (
10
+ ContentPart,
11
+ ImageURLPart,
12
+ TextPart,
13
+ ThinkPart,
14
+ ToolResult,
15
+ VideoURLPart,
16
+ )
17
+
18
+
19
+ def system(message: str) -> ContentPart:
20
+ return TextPart(text=f"<system>{message}</system>")
21
+
22
+
23
+ def system_reminder(message: str) -> TextPart:
24
+ return TextPart(text=f"<system-reminder>\n{message}\n</system-reminder>")
25
+
26
+
27
+ def is_system_reminder_message(message: Message) -> bool:
28
+ """Check whether a message is an internal system-reminder user message."""
29
+ if message.role != "user" or len(message.content) != 1:
30
+ return False
31
+ part = message.content[0]
32
+ return isinstance(part, TextPart) and part.text.strip().startswith("<system-reminder>")
33
+
34
+
35
+ def tool_result_to_message(tool_result: ToolResult) -> Message:
36
+ """Convert a tool result to a message."""
37
+ if tool_result.return_value.is_error:
38
+ assert tool_result.return_value.message, "Error return value should have a message"
39
+ message = tool_result.return_value.message
40
+ if isinstance(tool_result.return_value, ToolRuntimeError):
41
+ message += "\nThis is an unexpected error and the tool is probably not working."
42
+ content: list[ContentPart] = [system(f"ERROR: {message}")]
43
+ if tool_result.return_value.output:
44
+ content.extend(_output_to_content_parts(tool_result.return_value.output))
45
+ else:
46
+ content: list[ContentPart] = []
47
+ if tool_result.return_value.message:
48
+ content.append(system(tool_result.return_value.message))
49
+ if tool_result.return_value.output:
50
+ content.extend(_output_to_content_parts(tool_result.return_value.output))
51
+ if not content:
52
+ content.append(system("Tool output is empty."))
53
+ elif not any(isinstance(part, TextPart) for part in content):
54
+ # Ensure at least one TextPart exists so the LLM API won't reject
55
+ # the message with "text content is empty" (see #1663).
56
+ content.insert(0, system("Tool returned non-text content."))
57
+
58
+ return Message(
59
+ role="tool",
60
+ content=content,
61
+ tool_call_id=tool_result.tool_call_id,
62
+ )
63
+
64
+
65
+ def _output_to_content_parts(
66
+ output: str | ContentPart | Sequence[ContentPart],
67
+ ) -> list[ContentPart]:
68
+ content: list[ContentPart] = []
69
+ match output:
70
+ case str(text):
71
+ if text:
72
+ content.append(TextPart(text=text))
73
+ case ContentPart():
74
+ content.append(output)
75
+ case _:
76
+ content.extend(output)
77
+ return content
78
+
79
+
80
+ def check_message(
81
+ message: Message, model_capabilities: set[ModelCapability]
82
+ ) -> set[ModelCapability]:
83
+ """Check the message content, return the missing model capabilities."""
84
+ capabilities_needed = set[ModelCapability]()
85
+ for part in message.content:
86
+ if isinstance(part, ImageURLPart):
87
+ capabilities_needed.add("image_in")
88
+ elif isinstance(part, VideoURLPart):
89
+ capabilities_needed.add("video_in")
90
+ elif isinstance(part, ThinkPart):
91
+ capabilities_needed.add("thinking")
92
+ return capabilities_needed - model_capabilities
kimi_cli/soul/slash.py ADDED
@@ -0,0 +1,341 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ from collections.abc import Awaitable, Callable
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from kaos.path import KaosPath
9
+ from kosong.message import Message
10
+
11
+ import kimi_cli.prompts as prompts
12
+ from kimi_cli import logger
13
+ from kimi_cli.soul import wire_send
14
+ from kimi_cli.soul.agent import load_agents_md
15
+ from kimi_cli.soul.context import Context
16
+ from kimi_cli.soul.dynamic_injections.afk_mode import AFK_DISABLED_REMINDER
17
+ from kimi_cli.soul.message import system, system_reminder
18
+ from kimi_cli.utils.export import is_sensitive_file
19
+ from kimi_cli.utils.path import sanitize_cli_path, shorten_home
20
+ from kimi_cli.utils.slashcmd import SlashCommandRegistry
21
+ from kimi_cli.wire.types import StatusUpdate, TextPart
22
+
23
+ if TYPE_CHECKING:
24
+ from kimi_cli.soul.kimisoul import KimiSoul
25
+
26
+ type SoulSlashCmdFunc = Callable[[KimiSoul, str], None | Awaitable[None]]
27
+ """
28
+ A function that runs as a KimiSoul-level slash command.
29
+
30
+ Raises:
31
+ Any exception that can be raised by `Soul.run`.
32
+ """
33
+
34
+ registry = SlashCommandRegistry[SoulSlashCmdFunc]()
35
+
36
+
37
+ @registry.command
38
+ async def init(soul: KimiSoul, args: str):
39
+ """Analyze the codebase and generate an `AGENTS.md` file"""
40
+ from kimi_cli.soul.kimisoul import KimiSoul
41
+
42
+ with tempfile.TemporaryDirectory() as temp_dir:
43
+ tmp_context = Context(file_backend=Path(temp_dir) / "context.jsonl")
44
+ tmp_soul = KimiSoul(soul.agent, context=tmp_context)
45
+ await tmp_soul.run(prompts.INIT)
46
+
47
+ agents_md = await load_agents_md(soul.runtime.builtin_args.KIMI_WORK_DIR)
48
+ system_message = system(
49
+ "The user just ran `/init` slash command. "
50
+ "The system has analyzed the codebase and generated an `AGENTS.md` file. "
51
+ f"Latest AGENTS.md file content:\n{agents_md}"
52
+ )
53
+ await soul.context.append_message(Message(role="user", content=[system_message]))
54
+ from kimi_cli.telemetry import track
55
+
56
+ track("init_complete")
57
+
58
+
59
+ @registry.command
60
+ async def compact(soul: KimiSoul, args: str):
61
+ """Compact the context (optionally with a custom focus, e.g. /compact keep db discussions)"""
62
+ if soul.context.n_checkpoints == 0:
63
+ wire_send(TextPart(text="The context is empty."))
64
+ return
65
+
66
+ logger.info("Running `/compact`")
67
+ instruction = args.strip()
68
+ await soul.compact_context(manual=True, custom_instruction=instruction)
69
+ wire_send(TextPart(text="The context has been compacted."))
70
+ snap = soul.status
71
+ wire_send(
72
+ StatusUpdate(
73
+ context_usage=snap.context_usage,
74
+ context_tokens=snap.context_tokens,
75
+ max_context_tokens=snap.max_context_tokens,
76
+ )
77
+ )
78
+
79
+
80
+ @registry.command(aliases=["reset"])
81
+ async def clear(soul: KimiSoul, args: str):
82
+ """Clear the context"""
83
+ logger.info("Running `/clear`")
84
+ await soul.context.clear()
85
+ await soul.context.write_system_prompt(soul.agent.system_prompt)
86
+ wire_send(TextPart(text="The context has been cleared."))
87
+ snap = soul.status
88
+ wire_send(
89
+ StatusUpdate(
90
+ context_usage=snap.context_usage,
91
+ context_tokens=snap.context_tokens,
92
+ max_context_tokens=snap.max_context_tokens,
93
+ )
94
+ )
95
+
96
+
97
+ @registry.command
98
+ async def yolo(soul: KimiSoul, args: str):
99
+ """Toggle YOLO mode (auto-approve all actions)"""
100
+ from kimi_cli.telemetry import track
101
+
102
+ # Inspect only the yolo flag: afk is independent and is toggled by /afk.
103
+ if soul.runtime.approval.is_yolo_flag():
104
+ soul.runtime.approval.set_yolo(False)
105
+ track("yolo_toggle", enabled=False)
106
+ if soul.runtime.approval.is_afk():
107
+ # Yolo off but afk still on -> tool calls remain auto-approved.
108
+ # Don't mislead the user into thinking approvals just came back.
109
+ wire_send(
110
+ TextPart(
111
+ text=(
112
+ "Yolo disabled, but afk is still on — tool calls remain "
113
+ "auto-approved. Use /afk to turn off afk."
114
+ )
115
+ )
116
+ )
117
+ else:
118
+ wire_send(TextPart(text="You only die once! Actions will require approval."))
119
+ else:
120
+ soul.runtime.approval.set_yolo(True)
121
+ track("yolo_toggle", enabled=True)
122
+ wire_send(TextPart(text="You only live once! All actions will be auto-approved."))
123
+
124
+
125
+ @registry.command
126
+ async def afk(soul: KimiSoul, args: str):
127
+ """Toggle afk mode (auto-dismiss AskUserQuestion, auto-approve tool calls)"""
128
+ from kimi_cli.telemetry import track
129
+
130
+ if soul.runtime.approval.is_afk():
131
+ soul.runtime.approval.set_afk(False)
132
+ await soul.notify_afk_changed(False)
133
+ await soul.context.append_message(
134
+ Message(role="user", content=[system_reminder(AFK_DISABLED_REMINDER)])
135
+ )
136
+ track("afk_toggle", enabled=False)
137
+ if soul.runtime.approval.is_yolo_flag():
138
+ wire_send(
139
+ TextPart(
140
+ text=("afk mode disabled. You are back at the terminal. Yolo is still on.")
141
+ )
142
+ )
143
+ else:
144
+ wire_send(TextPart(text="afk mode disabled. You are back at the terminal."))
145
+ else:
146
+ soul.runtime.approval.set_afk(True)
147
+ await soul.notify_afk_changed(True)
148
+ track("afk_toggle", enabled=True)
149
+ wire_send(
150
+ TextPart(
151
+ text=(
152
+ "afk mode enabled. AskUserQuestion will be auto-dismissed "
153
+ "and tool calls auto-approved."
154
+ )
155
+ )
156
+ )
157
+
158
+
159
+ @registry.command
160
+ async def plan(soul: KimiSoul, args: str):
161
+ """Toggle plan mode. Usage: /plan [on|off|view|clear]"""
162
+ subcmd = args.strip().lower()
163
+
164
+ if subcmd == "on":
165
+ if not soul.plan_mode:
166
+ await soul.toggle_plan_mode_from_manual()
167
+ plan_path = soul.get_plan_file_path()
168
+ wire_send(TextPart(text=f"Plan mode ON. Plan file: {plan_path}"))
169
+ wire_send(StatusUpdate(plan_mode=soul.plan_mode))
170
+ elif subcmd == "off":
171
+ if soul.plan_mode:
172
+ await soul.toggle_plan_mode_from_manual()
173
+ wire_send(TextPart(text="Plan mode OFF. All tools are now available."))
174
+ wire_send(StatusUpdate(plan_mode=soul.plan_mode))
175
+ elif subcmd == "view":
176
+ content = soul.read_current_plan()
177
+ if content:
178
+ wire_send(TextPart(text=content))
179
+ else:
180
+ wire_send(TextPart(text="No plan file found for this session."))
181
+ elif subcmd == "clear":
182
+ soul.clear_current_plan()
183
+ wire_send(TextPart(text="Plan cleared."))
184
+ else:
185
+ # Default: toggle
186
+ new_state = await soul.toggle_plan_mode_from_manual()
187
+ if new_state:
188
+ plan_path = soul.get_plan_file_path()
189
+ wire_send(
190
+ TextPart(
191
+ text=f"Plan mode ON. Write your plan to: {plan_path}\n"
192
+ "Use ExitPlanMode when done, or /plan off to exit manually."
193
+ )
194
+ )
195
+ else:
196
+ wire_send(TextPart(text="Plan mode OFF. All tools are now available."))
197
+ wire_send(StatusUpdate(plan_mode=soul.plan_mode))
198
+
199
+
200
+ @registry.command(name="add-dir")
201
+ async def add_dir(soul: KimiSoul, args: str):
202
+ """Add a directory to the workspace. Usage: /add-dir <path>. Run without args to list added dirs""" # noqa: E501
203
+ from kaos.path import KaosPath
204
+
205
+ from kimi_cli.utils.path import is_within_directory, list_directory
206
+
207
+ args = sanitize_cli_path(args)
208
+ if not args:
209
+ if not soul.runtime.additional_dirs:
210
+ wire_send(TextPart(text="No additional directories. Usage: /add-dir <path>"))
211
+ else:
212
+ lines = ["Additional directories:"]
213
+ for d in soul.runtime.additional_dirs:
214
+ lines.append(f" - {d}")
215
+ wire_send(TextPart(text="\n".join(lines)))
216
+ return
217
+
218
+ path = KaosPath(args).expanduser().canonical()
219
+
220
+ if not await path.exists():
221
+ wire_send(TextPart(text=f"Directory does not exist: {path}"))
222
+ return
223
+ if not await path.is_dir():
224
+ wire_send(TextPart(text=f"Not a directory: {path}"))
225
+ return
226
+
227
+ # Check if already added (exact match)
228
+ if path in soul.runtime.additional_dirs:
229
+ wire_send(TextPart(text=f"Directory already in workspace: {path}"))
230
+ return
231
+
232
+ # Check if it's within the work_dir (already accessible)
233
+ work_dir = soul.runtime.builtin_args.KIMI_WORK_DIR
234
+ if is_within_directory(path, work_dir):
235
+ wire_send(TextPart(text=f"Directory is already within the working directory: {path}"))
236
+ return
237
+
238
+ # Check if it's within an already-added additional directory (redundant)
239
+ for existing in soul.runtime.additional_dirs:
240
+ if is_within_directory(path, existing):
241
+ wire_send(
242
+ TextPart(
243
+ text=f"Directory is already within an added directory `{existing}`: {path}"
244
+ )
245
+ )
246
+ return
247
+
248
+ # Validate readability before committing any state changes
249
+ try:
250
+ ls_output = await list_directory(path)
251
+ except OSError as e:
252
+ wire_send(TextPart(text=f"Cannot read directory: {path} ({e})"))
253
+ return
254
+
255
+ # Add the directory (only after readability is confirmed)
256
+ soul.runtime.additional_dirs.append(path)
257
+
258
+ # Persist to session state
259
+ soul.runtime.session.state.additional_dirs.append(str(path))
260
+ soul.runtime.session.save_state()
261
+
262
+ # Inject a system message to inform the LLM about the new directory
263
+ system_message = system(
264
+ f"The user has added an additional directory to the workspace: `{path}`\n\n"
265
+ f"Directory listing:\n```\n{ls_output}\n```\n\n"
266
+ "You can now read, write, search, and glob files in this directory "
267
+ "as if it were part of the working directory."
268
+ )
269
+ await soul.context.append_message(Message(role="user", content=[system_message]))
270
+
271
+ wire_send(TextPart(text=f"Added directory to workspace: {path}"))
272
+ logger.info("Added additional directory: {path}", path=path)
273
+
274
+
275
+ @registry.command
276
+ async def export(soul: KimiSoul, args: str):
277
+ """Export current session context to a markdown file"""
278
+ from kimi_cli.utils.export import perform_export
279
+
280
+ session = soul.runtime.session
281
+ result = await perform_export(
282
+ history=list(soul.context.history),
283
+ session_id=session.id,
284
+ work_dir=str(session.work_dir),
285
+ token_count=soul.context.token_count,
286
+ args=args,
287
+ default_dir=Path(str(session.work_dir)),
288
+ )
289
+ if isinstance(result, str):
290
+ wire_send(TextPart(text=result))
291
+ return
292
+ output, count = result
293
+ display = shorten_home(KaosPath(str(output)))
294
+ wire_send(TextPart(text=f"Exported {count} messages to {display}"))
295
+ wire_send(
296
+ TextPart(
297
+ text=" Note: The exported file may contain sensitive information. "
298
+ "Please be cautious when sharing it externally."
299
+ )
300
+ )
301
+
302
+
303
+ @registry.command(name="import")
304
+ async def import_context(soul: KimiSoul, args: str):
305
+ """Import context from a file or session ID"""
306
+ from kimi_cli.utils.export import perform_import
307
+
308
+ target = sanitize_cli_path(args)
309
+ if not target:
310
+ wire_send(TextPart(text="Usage: /import <file_path or session_id>"))
311
+ return
312
+
313
+ session = soul.runtime.session
314
+ raw_max_context_size = (
315
+ soul.runtime.llm.max_context_size if soul.runtime.llm is not None else None
316
+ )
317
+ max_context_size = (
318
+ raw_max_context_size
319
+ if isinstance(raw_max_context_size, int) and raw_max_context_size > 0
320
+ else None
321
+ )
322
+ result = await perform_import(
323
+ target=target,
324
+ current_session_id=session.id,
325
+ work_dir=session.work_dir,
326
+ context=soul.context,
327
+ max_context_size=max_context_size,
328
+ )
329
+ if isinstance(result, str):
330
+ wire_send(TextPart(text=result))
331
+ return
332
+
333
+ source_desc, content_len = result
334
+ wire_send(TextPart(text=f"Imported context from {source_desc} ({content_len} chars)."))
335
+ if source_desc.startswith("file") and is_sensitive_file(Path(target).name):
336
+ wire_send(
337
+ TextPart(
338
+ text="Warning: This file may contain secrets (API keys, tokens, credentials). "
339
+ "The content is now part of your session context."
340
+ )
341
+ )