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/AGENTS.md ADDED
@@ -0,0 +1,91 @@
1
+ # ACP Integration Notes (kimi-cli)
2
+
3
+ ## Protocol summary (ACP overview)
4
+ - ACP is JSON-RPC 2.0 with request/response methods plus one-way notifications.
5
+ - Typical flow: `initialize` -> optional `authenticate` -> `session/new` or `session/load`
6
+ -> `session/prompt`
7
+ with `session/update` notifications and optional `session/cancel`.
8
+ - Clients provide `session/request_permission` and optional terminal/filesystem methods.
9
+ - All ACP file paths must be absolute; line numbers are 1-based.
10
+
11
+ ## Entry points and server modes
12
+ - **Single-session server**: `KimiCLI.run_acp()` uses `ACP` -> `ACPServerSingleSession`.
13
+ - Code: `src/kimi_cli/app.py`, `src/kimi_cli/ui/acp/__init__.py`.
14
+ - Used when running CLI with `--acp` UI mode.
15
+ - **Multi-session server**: `acp_main()` runs `ACPServer` with `use_unstable_protocol=True`.
16
+ - Code: `src/kimi_cli/acp/__init__.py`, `src/kimi_cli/acp/server.py`.
17
+ - Exposed via the `kimi acp` command in `src/kimi_cli/cli/__init__.py`.
18
+
19
+ ## Capabilities advertised
20
+ - `prompt_capabilities`: `embedded_context=False`, `image=True`, `audio=False`.
21
+ - `mcp_capabilities`: `http=True`, `sse=False`.
22
+ - Single-session: `load_session=False`, no session list capabilities.
23
+ - Multi-session: `load_session=True`, `session_capabilities.list` supported.
24
+ - `auth_methods=[]` (no authentication methods advertised).
25
+
26
+ ## Session lifecycle (implemented behavior)
27
+ - `session/new`
28
+ - Multi-session: creates a persisted `Session`, builds `KimiCLI`, stores `ACPSession`.
29
+ - Single-session: wraps the existing `Soul` into a `Wire` loop and creates `ACPSession`.
30
+ - Both send `AvailableCommandsUpdate` for slash commands on session creation.
31
+ - MCP servers passed by ACP are converted via `acp_mcp_servers_to_mcp_config`.
32
+ - `session/load`
33
+ - Multi-session only: loads by `Session.find`, then builds `KimiCLI` and `ACPSession`.
34
+ - No history replay yet (TODO).
35
+ - Single-session: not implemented.
36
+ - `session/list`
37
+ - Multi-session only: lists sessions via `Session.list`, no pagination.
38
+ - Single-session: not implemented.
39
+ - `session/prompt`
40
+ - Uses `ACPSession.prompt()` to stream updates and produce a `stop_reason`.
41
+ - Stop reasons: `end_turn`, `max_turn_requests`, `cancelled`.
42
+ - `session/cancel`
43
+ - Sets the per-turn cancel event to stop the prompt.
44
+
45
+ ## Streaming updates and content mapping
46
+ - Text chunks -> `AgentMessageChunk`.
47
+ - Think chunks -> `AgentThoughtChunk`.
48
+ - Tool calls:
49
+ - Start -> `ToolCallStart` with JSON args as text content.
50
+ - Streaming args -> `ToolCallProgress` with updated title/args.
51
+ - Results -> `ToolCallProgress` with `completed` or `failed`.
52
+ - Tool call IDs are prefixed with turn ID to avoid collisions across turns.
53
+ - Plan updates:
54
+ - `TodoDisplayBlock` is converted into `AgentPlanUpdate`.
55
+ - Available commands:
56
+ - `AvailableCommandsUpdate` is sent right after session creation.
57
+
58
+ ## Prompt/content conversion
59
+ - Incoming prompt blocks:
60
+ - Supported: `TextContentBlock`, `ImageContentBlock` (converted to data URL).
61
+ - Unsupported types are logged and ignored.
62
+ - Tool result display blocks:
63
+ - `DiffDisplayBlock` -> `FileEditToolCallContent`.
64
+ - `HideOutputDisplayBlock` suppresses tool output in ACP (used by terminal tool).
65
+
66
+ ## Tool integration and permission flow
67
+ - ACP sessions use `ACPKaos` to route filesystem reads/writes through ACP clients.
68
+ - If the client advertises `terminal` capability, the `Shell` tool is replaced by an
69
+ ACP-backed `Terminal` tool.
70
+ - Uses ACP `terminal/create`, waits for exit, streams `TerminalToolCallContent`,
71
+ then releases the terminal handle.
72
+ - Approval requests in the core tool system are bridged to ACP
73
+ `session/request_permission` with allow-once/allow-always/reject options.
74
+
75
+ ## Current gaps / not implemented
76
+ - `authenticate` method (not used by current Zed ACP client).
77
+ - `session/set_mode` and `session/set_model` (no multi-mode/model switching in kimi-cli).
78
+ - `ext_method` / `ext_notification` for custom ACP extensions are stubbed.
79
+ - Single-session server does not implement `session/load` or `session/list`.
80
+
81
+ ## Filesystem (ACP client-backed)
82
+ - When the client advertises `fs.readTextFile` / `fs.writeTextFile`, `ACPKaos` routes
83
+ reads and writes through ACP `fs/*` methods.
84
+ - `ReadFile` uses `KaosPath.read_lines`, which `ACPKaos` implements via ACP reads.
85
+ - `WriteFile` uses `KaosPath.read_text/write_text/append_text` and still generates diffs
86
+ and approvals in the tool layer.
87
+
88
+ ## Zed-specific notes (as of current integration)
89
+ - Zed does not currently call `authenticate`.
90
+ - Zed’s external agent server session management is not yet available, so
91
+ `session/load` is not exercised in practice.
@@ -0,0 +1,13 @@
1
+ def acp_main() -> None:
2
+ """Entry point for the multi-session ACP server."""
3
+ import asyncio
4
+
5
+ import acp
6
+
7
+ from kimi_cli.acp.server import ACPServer
8
+ from kimi_cli.app import enable_logging
9
+ from kimi_cli.utils.logging import logger
10
+
11
+ enable_logging()
12
+ logger.info("Starting ACP server on stdio")
13
+ asyncio.run(acp.run_agent(ACPServer(), use_unstable_protocol=True))
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import acp
4
+
5
+ from kimi_cli.acp.types import ACPContentBlock
6
+ from kimi_cli.utils.logging import logger
7
+ from kimi_cli.wire.types import (
8
+ ContentPart,
9
+ DiffDisplayBlock,
10
+ DisplayBlock,
11
+ ImageURLPart,
12
+ TextPart,
13
+ ToolReturnValue,
14
+ )
15
+
16
+
17
+ def acp_blocks_to_content_parts(prompt: list[ACPContentBlock]) -> list[ContentPart]:
18
+ content: list[ContentPart] = []
19
+ for block in prompt:
20
+ match block:
21
+ case acp.schema.TextContentBlock():
22
+ content.append(TextPart(text=block.text))
23
+ case acp.schema.ImageContentBlock():
24
+ content.append(
25
+ ImageURLPart(
26
+ image_url=ImageURLPart.ImageURL(
27
+ url=f"data:{block.mime_type};base64,{block.data}"
28
+ )
29
+ )
30
+ )
31
+ case _:
32
+ logger.warning("Unsupported prompt content block: {block}", block=block)
33
+ return content
34
+
35
+
36
+ def display_block_to_acp_content(
37
+ block: DisplayBlock,
38
+ ) -> acp.schema.FileEditToolCallContent | None:
39
+ if isinstance(block, DiffDisplayBlock):
40
+ return acp.schema.FileEditToolCallContent(
41
+ type="diff",
42
+ path=block.path,
43
+ old_text=block.old_text,
44
+ new_text=block.new_text,
45
+ )
46
+
47
+ return None
48
+
49
+
50
+ def tool_result_to_acp_content(
51
+ tool_ret: ToolReturnValue,
52
+ ) -> list[
53
+ acp.schema.ContentToolCallContent
54
+ | acp.schema.FileEditToolCallContent
55
+ | acp.schema.TerminalToolCallContent
56
+ ]:
57
+ from kimi_cli.acp.tools import HideOutputDisplayBlock
58
+
59
+ def _to_acp_content(
60
+ part: ContentPart,
61
+ ) -> (
62
+ acp.schema.ContentToolCallContent
63
+ | acp.schema.FileEditToolCallContent
64
+ | acp.schema.TerminalToolCallContent
65
+ ):
66
+ if isinstance(part, TextPart):
67
+ return acp.schema.ContentToolCallContent(
68
+ type="content", content=acp.schema.TextContentBlock(type="text", text=part.text)
69
+ )
70
+ logger.warning("Unsupported content part in tool result: {part}", part=part)
71
+ return acp.schema.ContentToolCallContent(
72
+ type="content",
73
+ content=acp.schema.TextContentBlock(type="text", text=f"[{part.__class__.__name__}]"),
74
+ )
75
+
76
+ def _to_text_block(text: str) -> acp.schema.ContentToolCallContent:
77
+ return acp.schema.ContentToolCallContent(
78
+ type="content", content=acp.schema.TextContentBlock(type="text", text=text)
79
+ )
80
+
81
+ contents: list[
82
+ acp.schema.ContentToolCallContent
83
+ | acp.schema.FileEditToolCallContent
84
+ | acp.schema.TerminalToolCallContent
85
+ ] = []
86
+
87
+ for block in tool_ret.display:
88
+ if isinstance(block, HideOutputDisplayBlock):
89
+ # return early to indicate no output should be shown
90
+ return []
91
+
92
+ content = display_block_to_acp_content(block)
93
+ if content is not None:
94
+ contents.append(content)
95
+ # TODO: better concatenation of `display` blocks and `output`?
96
+
97
+ output = tool_ret.output
98
+ if isinstance(output, str):
99
+ if output:
100
+ contents.append(_to_text_block(output))
101
+ else:
102
+ # NOTE: At the moment, ToolReturnValue.output is either a string or a
103
+ # list of ContentPart. We avoid an unnecessary isinstance() check here
104
+ # to keep pyright happy while still handling list outputs.
105
+ contents.extend(_to_acp_content(part) for part in output)
106
+
107
+ if not contents and tool_ret.message:
108
+ # Fallback to the `message` for LLM if there's no other content
109
+ contents.append(_to_text_block(tool_ret.message))
110
+
111
+ return contents
kimi_cli/acp/kaos.py ADDED
@@ -0,0 +1,270 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncGenerator, Iterable
5
+ from contextlib import suppress
6
+ from typing import Literal
7
+
8
+ import acp
9
+ from kaos import AsyncReadable, AsyncWritable, Kaos, KaosProcess, StatResult, StrOrKaosPath
10
+ from kaos.local import local_kaos
11
+ from kaos.path import KaosPath
12
+
13
+ _DEFAULT_TERMINAL_OUTPUT_LIMIT = 50_000
14
+ _DEFAULT_POLL_INTERVAL = 0.2
15
+ _TRUNCATION_NOTICE = "[acp output truncated]\n"
16
+
17
+
18
+ class _NullWritable:
19
+ def can_write_eof(self) -> bool:
20
+ return False
21
+
22
+ def close(self) -> None:
23
+ return None
24
+
25
+ async def drain(self) -> None:
26
+ return None
27
+
28
+ def is_closing(self) -> bool:
29
+ return False
30
+
31
+ async def wait_closed(self) -> None:
32
+ return None
33
+
34
+ def write(self, data: bytes) -> None:
35
+ return None
36
+
37
+ def writelines(self, data: Iterable[bytes], /) -> None:
38
+ return None
39
+
40
+ def write_eof(self) -> None:
41
+ return None
42
+
43
+
44
+ class ACPProcess:
45
+ """KAOS process adapter for ACP terminal execution."""
46
+
47
+ def __init__(
48
+ self,
49
+ terminal: acp.TerminalHandle,
50
+ *,
51
+ poll_interval: float = _DEFAULT_POLL_INTERVAL,
52
+ ) -> None:
53
+ self._terminal = terminal
54
+ self._poll_interval = poll_interval
55
+ self._stdin = _NullWritable()
56
+ self._stdout = asyncio.StreamReader()
57
+ self._stderr = asyncio.StreamReader()
58
+ self.stdin: AsyncWritable = self._stdin
59
+ self.stdout: AsyncReadable = self._stdout
60
+ # ACP does not expose stderr separately; keep stderr empty.
61
+ self.stderr: AsyncReadable = self._stderr
62
+ self._returncode: int | None = None
63
+ self._last_output = ""
64
+ self._truncation_noted = False
65
+ self._exit_future: asyncio.Future[int] = asyncio.get_running_loop().create_future()
66
+ self._poll_task = asyncio.create_task(self._poll_output())
67
+
68
+ @property
69
+ def pid(self) -> int:
70
+ return -1
71
+
72
+ @property
73
+ def returncode(self) -> int | None:
74
+ return self._returncode
75
+
76
+ async def wait(self) -> int:
77
+ return await self._exit_future
78
+
79
+ async def kill(self) -> None:
80
+ await self._terminal.kill()
81
+
82
+ def _feed_output(self, output_response: acp.schema.TerminalOutputResponse) -> None:
83
+ output = output_response.output
84
+ reset = output_response.truncated or (
85
+ self._last_output and not output.startswith(self._last_output)
86
+ )
87
+ if reset and self._last_output and not self._truncation_noted:
88
+ self._stdout.feed_data(_TRUNCATION_NOTICE.encode("utf-8"))
89
+ self._truncation_noted = True
90
+
91
+ delta = output if reset else output[len(self._last_output) :]
92
+ if delta:
93
+ self._stdout.feed_data(delta.encode("utf-8", "replace"))
94
+ self._last_output = output
95
+
96
+ @staticmethod
97
+ def _normalize_exit_code(exit_code: int | None) -> int:
98
+ return 1 if exit_code is None else exit_code
99
+
100
+ async def _poll_output(self) -> None:
101
+ exit_task = asyncio.create_task(self._terminal.wait_for_exit())
102
+ exit_code: int | None = None
103
+ try:
104
+ while True:
105
+ if exit_task.done():
106
+ exit_response = exit_task.result()
107
+ exit_code = exit_response.exit_code
108
+ break
109
+
110
+ output_response = await self._terminal.current_output()
111
+ self._feed_output(output_response)
112
+ if output_response.exit_status:
113
+ exit_code = output_response.exit_status.exit_code
114
+ try:
115
+ exit_response = await exit_task
116
+ exit_code = exit_response.exit_code or exit_code
117
+ except Exception:
118
+ pass
119
+ break
120
+
121
+ await asyncio.sleep(self._poll_interval)
122
+
123
+ final_output = await self._terminal.current_output()
124
+ self._feed_output(final_output)
125
+ except Exception as exc:
126
+ error_note = f"[acp terminal error] {exc}\n"
127
+ self._stdout.feed_data(error_note.encode("utf-8", "replace"))
128
+ if exit_code is None:
129
+ exit_code = 1
130
+ finally:
131
+ if not exit_task.done():
132
+ exit_task.cancel()
133
+ with suppress(Exception):
134
+ await exit_task
135
+ self._returncode = self._normalize_exit_code(exit_code)
136
+ self._stdout.feed_eof()
137
+ self._stderr.feed_eof()
138
+ if not self._exit_future.done():
139
+ self._exit_future.set_result(self._returncode)
140
+ with suppress(Exception):
141
+ await self._terminal.release()
142
+
143
+
144
+ class ACPKaos:
145
+ """KAOS backend that routes supported operations through ACP."""
146
+
147
+ name: str = "acp"
148
+
149
+ def __init__(
150
+ self,
151
+ client: acp.Client,
152
+ session_id: str,
153
+ client_capabilities: acp.schema.ClientCapabilities | None,
154
+ fallback: Kaos | None = None,
155
+ *,
156
+ output_byte_limit: int | None = _DEFAULT_TERMINAL_OUTPUT_LIMIT,
157
+ poll_interval: float = _DEFAULT_POLL_INTERVAL,
158
+ ) -> None:
159
+ self._client = client
160
+ self._session_id = session_id
161
+ self._fallback = fallback or local_kaos
162
+ fs = client_capabilities.fs if client_capabilities else None
163
+ self._supports_read = bool(fs and fs.read_text_file)
164
+ self._supports_write = bool(fs and fs.write_text_file)
165
+ self._supports_terminal = bool(client_capabilities and client_capabilities.terminal)
166
+ self._output_byte_limit = output_byte_limit
167
+ self._poll_interval = poll_interval
168
+
169
+ def pathclass(self):
170
+ return self._fallback.pathclass()
171
+
172
+ def normpath(self, path: StrOrKaosPath) -> KaosPath:
173
+ return self._fallback.normpath(path)
174
+
175
+ def gethome(self) -> KaosPath:
176
+ return self._fallback.gethome()
177
+
178
+ def getcwd(self) -> KaosPath:
179
+ return self._fallback.getcwd()
180
+
181
+ async def chdir(self, path: StrOrKaosPath) -> None:
182
+ await self._fallback.chdir(path)
183
+
184
+ async def stat(self, path: StrOrKaosPath, *, follow_symlinks: bool = True) -> StatResult:
185
+ return await self._fallback.stat(path, follow_symlinks=follow_symlinks)
186
+
187
+ def iterdir(self, path: StrOrKaosPath) -> AsyncGenerator[KaosPath]:
188
+ return self._fallback.iterdir(path)
189
+
190
+ def glob(
191
+ self, path: StrOrKaosPath, pattern: str, *, case_sensitive: bool = True
192
+ ) -> AsyncGenerator[KaosPath]:
193
+ return self._fallback.glob(path, pattern, case_sensitive=case_sensitive)
194
+
195
+ async def readbytes(self, path: StrOrKaosPath, n: int | None = None) -> bytes:
196
+ return await self._fallback.readbytes(path, n=n)
197
+
198
+ async def readtext(
199
+ self,
200
+ path: StrOrKaosPath,
201
+ *,
202
+ encoding: str = "utf-8",
203
+ errors: Literal["strict", "ignore", "replace"] = "strict",
204
+ ) -> str:
205
+ abs_path = self._abs_path(path)
206
+ if not self._supports_read:
207
+ return await self._fallback.readtext(abs_path, encoding=encoding, errors=errors)
208
+ response = await self._client.read_text_file(path=abs_path, session_id=self._session_id)
209
+ return response.content
210
+
211
+ async def readlines(
212
+ self,
213
+ path: StrOrKaosPath,
214
+ *,
215
+ encoding: str = "utf-8",
216
+ errors: Literal["strict", "ignore", "replace"] = "strict",
217
+ ) -> AsyncGenerator[str]:
218
+ text = await self.readtext(path, encoding=encoding, errors=errors)
219
+ for line in text.splitlines(keepends=True):
220
+ yield line
221
+
222
+ async def writebytes(self, path: StrOrKaosPath, data: bytes) -> int:
223
+ return await self._fallback.writebytes(path, data)
224
+
225
+ async def writetext(
226
+ self,
227
+ path: StrOrKaosPath,
228
+ data: str,
229
+ *,
230
+ mode: Literal["w", "a"] = "w",
231
+ encoding: str = "utf-8",
232
+ errors: Literal["strict", "ignore", "replace"] = "strict",
233
+ ) -> int:
234
+ abs_path = self._abs_path(path)
235
+ if mode == "a":
236
+ if self._supports_read and self._supports_write:
237
+ existing = await self.readtext(abs_path, encoding=encoding, errors=errors)
238
+ await self._client.write_text_file(
239
+ path=abs_path,
240
+ content=existing + data,
241
+ session_id=self._session_id,
242
+ )
243
+ return len(data)
244
+ return await self._fallback.writetext(
245
+ abs_path, data, mode="a", encoding=encoding, errors=errors
246
+ )
247
+
248
+ if not self._supports_write:
249
+ return await self._fallback.writetext(
250
+ abs_path, data, mode=mode, encoding=encoding, errors=errors
251
+ )
252
+
253
+ await self._client.write_text_file(
254
+ path=abs_path,
255
+ content=data,
256
+ session_id=self._session_id,
257
+ )
258
+ return len(data)
259
+
260
+ async def mkdir(
261
+ self, path: StrOrKaosPath, parents: bool = False, exist_ok: bool = False
262
+ ) -> None:
263
+ await self._fallback.mkdir(path, parents=parents, exist_ok=exist_ok)
264
+
265
+ async def exec(self, *args: str) -> KaosProcess:
266
+ return await self._fallback.exec(*args)
267
+
268
+ def _abs_path(self, path: StrOrKaosPath) -> str:
269
+ kaos_path = path if isinstance(path, KaosPath) else KaosPath(path)
270
+ return str(kaos_path.canonical())
kimi_cli/acp/mcp.py ADDED
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import acp.schema
6
+ from fastmcp.mcp_config import MCPConfig
7
+ from pydantic import ValidationError
8
+
9
+ from kimi_cli.acp.types import MCPServer
10
+ from kimi_cli.exception import MCPConfigError
11
+
12
+
13
+ def acp_mcp_servers_to_mcp_config(mcp_servers: list[MCPServer]) -> MCPConfig:
14
+ if not mcp_servers:
15
+ return MCPConfig()
16
+
17
+ try:
18
+ return MCPConfig.model_validate(
19
+ {"mcpServers": {server.name: _convert_acp_mcp_server(server) for server in mcp_servers}}
20
+ )
21
+ except ValidationError as exc:
22
+ raise MCPConfigError(f"Invalid MCP config from ACP client: {exc}") from exc
23
+
24
+
25
+ def _convert_acp_mcp_server(server: MCPServer) -> dict[str, Any]:
26
+ """Convert an ACP MCP server to a dictionary representation."""
27
+ match server:
28
+ case acp.schema.HttpMcpServer():
29
+ return {
30
+ "url": server.url,
31
+ "transport": "http",
32
+ "headers": {header.name: header.value for header in server.headers},
33
+ }
34
+ case acp.schema.SseMcpServer():
35
+ return {
36
+ "url": server.url,
37
+ "transport": "sse",
38
+ "headers": {header.name: header.value for header in server.headers},
39
+ }
40
+ case acp.schema.McpServerStdio():
41
+ return {
42
+ "command": server.command,
43
+ "args": server.args,
44
+ "env": {item.name: item.value for item in server.env},
45
+ "transport": "stdio",
46
+ }