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.
- bhu_cli-1.46.0.dist-info/METADATA +210 -0
- bhu_cli-1.46.0.dist-info/RECORD +258 -0
- bhu_cli-1.46.0.dist-info/WHEEL +4 -0
- bhu_cli-1.46.0.dist-info/entry_points.txt +4 -0
- kimi_cli/CHANGELOG.md +1281 -0
- kimi_cli/__init__.py +29 -0
- kimi_cli/__main__.py +43 -0
- kimi_cli/_build_info.py +1 -0
- kimi_cli/acp/AGENTS.md +92 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +128 -0
- kimi_cli/acp/kaos.py +291 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +468 -0
- kimi_cli/acp/session.py +499 -0
- kimi_cli/acp/tools.py +167 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/acp/version.py +45 -0
- kimi_cli/agents/default/agent.yaml +36 -0
- kimi_cli/agents/default/coder.yaml +25 -0
- kimi_cli/agents/default/explore.yaml +46 -0
- kimi_cli/agents/default/plan.yaml +30 -0
- kimi_cli/agents/default/system.md +160 -0
- kimi_cli/agents/okabe/agent.yaml +22 -0
- kimi_cli/agentspec.py +160 -0
- kimi_cli/app.py +807 -0
- kimi_cli/approval_runtime/__init__.py +29 -0
- kimi_cli/approval_runtime/models.py +42 -0
- kimi_cli/approval_runtime/runtime.py +235 -0
- kimi_cli/auth/__init__.py +5 -0
- kimi_cli/auth/oauth.py +1092 -0
- kimi_cli/auth/platforms.py +374 -0
- kimi_cli/background/__init__.py +36 -0
- kimi_cli/background/agent_runner.py +231 -0
- kimi_cli/background/ids.py +19 -0
- kimi_cli/background/manager.py +725 -0
- kimi_cli/background/models.py +105 -0
- kimi_cli/background/store.py +237 -0
- kimi_cli/background/summary.py +66 -0
- kimi_cli/background/worker.py +209 -0
- kimi_cli/cli/__init__.py +1081 -0
- kimi_cli/cli/__main__.py +34 -0
- kimi_cli/cli/_lazy_group.py +238 -0
- kimi_cli/cli/export.py +322 -0
- kimi_cli/cli/info.py +62 -0
- kimi_cli/cli/mcp.py +353 -0
- kimi_cli/cli/plugin.py +347 -0
- kimi_cli/cli/toad.py +73 -0
- kimi_cli/cli/vis.py +38 -0
- kimi_cli/cli/web.py +80 -0
- kimi_cli/config.py +429 -0
- kimi_cli/constant.py +116 -0
- kimi_cli/exception.py +43 -0
- kimi_cli/hooks/__init__.py +24 -0
- kimi_cli/hooks/config.py +34 -0
- kimi_cli/hooks/defaults/rtk.py +48 -0
- kimi_cli/hooks/engine.py +394 -0
- kimi_cli/hooks/events.py +190 -0
- kimi_cli/hooks/runner.py +103 -0
- kimi_cli/llm.py +333 -0
- kimi_cli/mcp_oauth.py +72 -0
- kimi_cli/metadata.py +79 -0
- kimi_cli/notifications/__init__.py +33 -0
- kimi_cli/notifications/llm.py +77 -0
- kimi_cli/notifications/manager.py +145 -0
- kimi_cli/notifications/models.py +50 -0
- kimi_cli/notifications/notifier.py +41 -0
- kimi_cli/notifications/store.py +118 -0
- kimi_cli/notifications/wire.py +21 -0
- kimi_cli/plugin/__init__.py +124 -0
- kimi_cli/plugin/manager.py +153 -0
- kimi_cli/plugin/tool.py +173 -0
- kimi_cli/prompts/__init__.py +6 -0
- kimi_cli/prompts/compact.md +73 -0
- kimi_cli/prompts/init.md +21 -0
- kimi_cli/py.typed +0 -0
- kimi_cli/session.py +319 -0
- kimi_cli/session_fork.py +325 -0
- kimi_cli/session_state.py +132 -0
- kimi_cli/share.py +14 -0
- kimi_cli/skill/__init__.py +727 -0
- kimi_cli/skill/flow/__init__.py +99 -0
- kimi_cli/skill/flow/d2.py +482 -0
- kimi_cli/skill/flow/mermaid.py +266 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +367 -0
- kimi_cli/soul/__init__.py +304 -0
- kimi_cli/soul/agent.py +519 -0
- kimi_cli/soul/approval.py +267 -0
- kimi_cli/soul/btw.py +237 -0
- kimi_cli/soul/compaction.py +189 -0
- kimi_cli/soul/context.py +339 -0
- kimi_cli/soul/denwarenji.py +39 -0
- kimi_cli/soul/dynamic_injection.py +84 -0
- kimi_cli/soul/dynamic_injections/__init__.py +0 -0
- kimi_cli/soul/dynamic_injections/afk_mode.py +74 -0
- kimi_cli/soul/dynamic_injections/plan_mode.py +245 -0
- kimi_cli/soul/kimisoul.py +1710 -0
- kimi_cli/soul/message.py +92 -0
- kimi_cli/soul/slash.py +341 -0
- kimi_cli/soul/toolset.py +891 -0
- kimi_cli/subagents/__init__.py +21 -0
- kimi_cli/subagents/builder.py +42 -0
- kimi_cli/subagents/core.py +86 -0
- kimi_cli/subagents/git_context.py +170 -0
- kimi_cli/subagents/models.py +54 -0
- kimi_cli/subagents/output.py +71 -0
- kimi_cli/subagents/registry.py +28 -0
- kimi_cli/subagents/runner.py +428 -0
- kimi_cli/subagents/store.py +196 -0
- kimi_cli/telemetry/__init__.py +197 -0
- kimi_cli/telemetry/crash.py +149 -0
- kimi_cli/telemetry/sink.py +143 -0
- kimi_cli/telemetry/transport.py +318 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +105 -0
- kimi_cli/tools/agent/__init__.py +277 -0
- kimi_cli/tools/agent/description.md +41 -0
- kimi_cli/tools/ask_user/__init__.py +154 -0
- kimi_cli/tools/ask_user/description.md +19 -0
- kimi_cli/tools/background/__init__.py +318 -0
- kimi_cli/tools/background/list.md +10 -0
- kimi_cli/tools/background/output.md +11 -0
- kimi_cli/tools/background/stop.md +8 -0
- kimi_cli/tools/display.py +46 -0
- kimi_cli/tools/dmail/__init__.py +38 -0
- kimi_cli/tools/dmail/dmail.md +17 -0
- kimi_cli/tools/file/__init__.py +30 -0
- kimi_cli/tools/file/glob.md +21 -0
- kimi_cli/tools/file/glob.py +178 -0
- kimi_cli/tools/file/grep.md +6 -0
- kimi_cli/tools/file/grep_local.py +590 -0
- kimi_cli/tools/file/plan_mode.py +45 -0
- kimi_cli/tools/file/read.md +16 -0
- kimi_cli/tools/file/read.py +300 -0
- kimi_cli/tools/file/read_media.md +24 -0
- kimi_cli/tools/file/read_media.py +217 -0
- kimi_cli/tools/file/replace.md +7 -0
- kimi_cli/tools/file/replace.py +195 -0
- kimi_cli/tools/file/utils.py +257 -0
- kimi_cli/tools/file/write.md +5 -0
- kimi_cli/tools/file/write.py +177 -0
- kimi_cli/tools/plan/__init__.py +339 -0
- kimi_cli/tools/plan/description.md +29 -0
- kimi_cli/tools/plan/enter.py +197 -0
- kimi_cli/tools/plan/enter_description.md +35 -0
- kimi_cli/tools/plan/heroes.py +277 -0
- kimi_cli/tools/shell/__init__.py +260 -0
- kimi_cli/tools/shell/bash.md +35 -0
- kimi_cli/tools/test.py +55 -0
- kimi_cli/tools/think/__init__.py +21 -0
- kimi_cli/tools/think/think.md +1 -0
- kimi_cli/tools/todo/__init__.py +168 -0
- kimi_cli/tools/todo/set_todo_list.md +23 -0
- kimi_cli/tools/utils.py +199 -0
- kimi_cli/tools/web/__init__.py +4 -0
- kimi_cli/tools/web/fetch.md +1 -0
- kimi_cli/tools/web/fetch.py +189 -0
- kimi_cli/tools/web/search.md +1 -0
- kimi_cli/tools/web/search.py +163 -0
- kimi_cli/ui/__init__.py +0 -0
- kimi_cli/ui/acp/__init__.py +99 -0
- kimi_cli/ui/print/__init__.py +474 -0
- kimi_cli/ui/print/visualize.py +194 -0
- kimi_cli/ui/shell/__init__.py +1540 -0
- kimi_cli/ui/shell/console.py +109 -0
- kimi_cli/ui/shell/debug.py +190 -0
- kimi_cli/ui/shell/echo.py +17 -0
- kimi_cli/ui/shell/export_import.py +117 -0
- kimi_cli/ui/shell/keyboard.py +300 -0
- kimi_cli/ui/shell/mcp_status.py +111 -0
- kimi_cli/ui/shell/oauth.py +149 -0
- kimi_cli/ui/shell/placeholders.py +531 -0
- kimi_cli/ui/shell/prompt.py +2259 -0
- kimi_cli/ui/shell/replay.py +215 -0
- kimi_cli/ui/shell/session_picker.py +227 -0
- kimi_cli/ui/shell/setup.py +212 -0
- kimi_cli/ui/shell/slash.py +893 -0
- kimi_cli/ui/shell/startup.py +32 -0
- kimi_cli/ui/shell/task_browser.py +486 -0
- kimi_cli/ui/shell/update.py +349 -0
- kimi_cli/ui/shell/usage.py +295 -0
- kimi_cli/ui/shell/visualize/__init__.py +165 -0
- kimi_cli/ui/shell/visualize/_approval_panel.py +505 -0
- kimi_cli/ui/shell/visualize/_blocks.py +640 -0
- kimi_cli/ui/shell/visualize/_btw_panel.py +224 -0
- kimi_cli/ui/shell/visualize/_input_router.py +48 -0
- kimi_cli/ui/shell/visualize/_interactive.py +524 -0
- kimi_cli/ui/shell/visualize/_live_view.py +902 -0
- kimi_cli/ui/shell/visualize/_question_panel.py +586 -0
- kimi_cli/ui/theme.py +241 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +24 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +108 -0
- kimi_cli/utils/clipboard.py +246 -0
- kimi_cli/utils/datetime.py +64 -0
- kimi_cli/utils/diff.py +135 -0
- kimi_cli/utils/editor.py +91 -0
- kimi_cli/utils/environment.py +225 -0
- kimi_cli/utils/envvar.py +22 -0
- kimi_cli/utils/export.py +696 -0
- kimi_cli/utils/file_filter.py +367 -0
- kimi_cli/utils/frontmatter.py +70 -0
- kimi_cli/utils/io.py +27 -0
- kimi_cli/utils/logging.py +124 -0
- kimi_cli/utils/media_tags.py +29 -0
- kimi_cli/utils/message.py +24 -0
- kimi_cli/utils/path.py +245 -0
- kimi_cli/utils/proctitle.py +33 -0
- kimi_cli/utils/proxy.py +31 -0
- kimi_cli/utils/pyinstaller.py +43 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/diff_render.py +481 -0
- kimi_cli/utils/rich/markdown.py +898 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/rich/syntax.py +114 -0
- kimi_cli/utils/sensitive.py +54 -0
- kimi_cli/utils/server.py +121 -0
- kimi_cli/utils/shell_quoting.py +38 -0
- kimi_cli/utils/signals.py +43 -0
- kimi_cli/utils/slashcmd.py +145 -0
- kimi_cli/utils/string.py +41 -0
- kimi_cli/utils/subprocess_env.py +73 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/utils/windows_paths.py +48 -0
- kimi_cli/vis/__init__.py +0 -0
- kimi_cli/vis/api/__init__.py +5 -0
- kimi_cli/vis/api/sessions.py +687 -0
- kimi_cli/vis/api/statistics.py +209 -0
- kimi_cli/vis/api/system.py +19 -0
- kimi_cli/vis/app.py +175 -0
- kimi_cli/web/__init__.py +5 -0
- kimi_cli/web/api/__init__.py +15 -0
- kimi_cli/web/api/config.py +208 -0
- kimi_cli/web/api/open_in.py +197 -0
- kimi_cli/web/api/sessions.py +1223 -0
- kimi_cli/web/app.py +451 -0
- kimi_cli/web/auth.py +191 -0
- kimi_cli/web/models.py +98 -0
- kimi_cli/web/runner/__init__.py +5 -0
- kimi_cli/web/runner/messages.py +57 -0
- kimi_cli/web/runner/process.py +754 -0
- kimi_cli/web/runner/worker.py +95 -0
- kimi_cli/web/store/__init__.py +1 -0
- kimi_cli/web/store/sessions.py +432 -0
- kimi_cli/wire/__init__.py +148 -0
- kimi_cli/wire/file.py +151 -0
- kimi_cli/wire/jsonrpc.py +263 -0
- kimi_cli/wire/protocol.py +2 -0
- kimi_cli/wire/root_hub.py +27 -0
- kimi_cli/wire/serde.py +26 -0
- kimi_cli/wire/server.py +1060 -0
- kimi_cli/wire/types.py +717 -0
kimi_cli/soul/message.py
ADDED
|
@@ -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
|
+
)
|