sahya-code 1.0.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.
- sahya_code/CHANGELOG.md +108 -0
- sahya_code/__init__.py +29 -0
- sahya_code/__main__.py +30 -0
- sahya_code/acp/AGENTS.md +92 -0
- sahya_code/acp/__init__.py +13 -0
- sahya_code/acp/convert.py +128 -0
- sahya_code/acp/kaos.py +291 -0
- sahya_code/acp/mcp.py +46 -0
- sahya_code/acp/server.py +457 -0
- sahya_code/acp/session.py +496 -0
- sahya_code/acp/tools.py +167 -0
- sahya_code/acp/types.py +13 -0
- sahya_code/acp/version.py +45 -0
- sahya_code/agents/default/agent.yaml +36 -0
- sahya_code/agents/default/coder.yaml +25 -0
- sahya_code/agents/default/explore.yaml +46 -0
- sahya_code/agents/default/plan.yaml +30 -0
- sahya_code/agents/default/system.md +160 -0
- sahya_code/agents/okabe/agent.yaml +22 -0
- sahya_code/agentspec.py +160 -0
- sahya_code/app.py +540 -0
- sahya_code/approval_runtime/__init__.py +29 -0
- sahya_code/approval_runtime/models.py +42 -0
- sahya_code/approval_runtime/runtime.py +189 -0
- sahya_code/auth/__init__.py +5 -0
- sahya_code/auth/oauth.py +804 -0
- sahya_code/auth/platforms.py +293 -0
- sahya_code/background/__init__.py +36 -0
- sahya_code/background/agent_runner.py +209 -0
- sahya_code/background/ids.py +19 -0
- sahya_code/background/manager.py +580 -0
- sahya_code/background/models.py +105 -0
- sahya_code/background/store.py +196 -0
- sahya_code/background/summary.py +66 -0
- sahya_code/background/worker.py +209 -0
- sahya_code/cli/__init__.py +932 -0
- sahya_code/cli/__main__.py +8 -0
- sahya_code/cli/_lazy_group.py +222 -0
- sahya_code/cli/export.py +74 -0
- sahya_code/cli/info.py +62 -0
- sahya_code/cli/mcp.py +349 -0
- sahya_code/cli/plugin.py +302 -0
- sahya_code/cli/toad.py +73 -0
- sahya_code/cli/vis.py +38 -0
- sahya_code/cli/web.py +80 -0
- sahya_code/config.py +391 -0
- sahya_code/constant.py +33 -0
- sahya_code/exception.py +43 -0
- sahya_code/hooks/__init__.py +4 -0
- sahya_code/hooks/config.py +34 -0
- sahya_code/hooks/engine.py +310 -0
- sahya_code/hooks/events.py +190 -0
- sahya_code/hooks/runner.py +89 -0
- sahya_code/llm.py +306 -0
- sahya_code/metadata.py +79 -0
- sahya_code/notifications/__init__.py +33 -0
- sahya_code/notifications/llm.py +77 -0
- sahya_code/notifications/manager.py +145 -0
- sahya_code/notifications/models.py +50 -0
- sahya_code/notifications/notifier.py +41 -0
- sahya_code/notifications/store.py +99 -0
- sahya_code/notifications/wire.py +21 -0
- sahya_code/plugin/__init__.py +124 -0
- sahya_code/plugin/manager.py +153 -0
- sahya_code/plugin/tool.py +173 -0
- sahya_code/prompts/__init__.py +6 -0
- sahya_code/prompts/compact.md +73 -0
- sahya_code/prompts/init.md +21 -0
- sahya_code/py.typed +0 -0
- sahya_code/session.py +293 -0
- sahya_code/session_state.py +42 -0
- sahya_code/share.py +28 -0
- sahya_code/skill/__init__.py +311 -0
- sahya_code/skill/flow/__init__.py +99 -0
- sahya_code/skill/flow/d2.py +482 -0
- sahya_code/skill/flow/mermaid.py +266 -0
- sahya_code/skills/kimi-cli-help/SKILL.md +55 -0
- sahya_code/skills/skill-creator/SKILL.md +367 -0
- sahya_code/soul/__init__.py +288 -0
- sahya_code/soul/agent.py +423 -0
- sahya_code/soul/approval.py +171 -0
- sahya_code/soul/compaction.py +189 -0
- sahya_code/soul/context.py +239 -0
- sahya_code/soul/denwarenji.py +39 -0
- sahya_code/soul/dynamic_injection.py +66 -0
- sahya_code/soul/dynamic_injections/__init__.py +0 -0
- sahya_code/soul/dynamic_injections/plan_mode.py +238 -0
- sahya_code/soul/dynamic_injections/yolo_mode.py +41 -0
- sahya_code/soul/message.py +92 -0
- sahya_code/soul/sahyasoul.py +1220 -0
- sahya_code/soul/slash.py +285 -0
- sahya_code/soul/toolset.py +610 -0
- sahya_code/subagents/__init__.py +21 -0
- sahya_code/subagents/builder.py +42 -0
- sahya_code/subagents/core.py +86 -0
- sahya_code/subagents/git_context.py +170 -0
- sahya_code/subagents/models.py +54 -0
- sahya_code/subagents/output.py +71 -0
- sahya_code/subagents/registry.py +28 -0
- sahya_code/subagents/runner.py +370 -0
- sahya_code/subagents/store.py +148 -0
- sahya_code/tools/AGENTS.md +5 -0
- sahya_code/tools/__init__.py +105 -0
- sahya_code/tools/agent/__init__.py +276 -0
- sahya_code/tools/agent/description.md +41 -0
- sahya_code/tools/ask_user/__init__.py +154 -0
- sahya_code/tools/ask_user/description.md +19 -0
- sahya_code/tools/background/__init__.py +318 -0
- sahya_code/tools/background/list.md +10 -0
- sahya_code/tools/background/output.md +11 -0
- sahya_code/tools/background/stop.md +8 -0
- sahya_code/tools/display.py +46 -0
- sahya_code/tools/dmail/__init__.py +38 -0
- sahya_code/tools/dmail/dmail.md +17 -0
- sahya_code/tools/file/__init__.py +30 -0
- sahya_code/tools/file/glob.md +17 -0
- sahya_code/tools/file/glob.py +156 -0
- sahya_code/tools/file/grep.md +5 -0
- sahya_code/tools/file/grep_local.py +524 -0
- sahya_code/tools/file/plan_mode.py +45 -0
- sahya_code/tools/file/read.md +14 -0
- sahya_code/tools/file/read.py +189 -0
- sahya_code/tools/file/read_media.md +24 -0
- sahya_code/tools/file/read_media.py +215 -0
- sahya_code/tools/file/replace.md +7 -0
- sahya_code/tools/file/replace.py +193 -0
- sahya_code/tools/file/utils.py +257 -0
- sahya_code/tools/file/write.md +5 -0
- sahya_code/tools/file/write.py +175 -0
- sahya_code/tools/plan/__init__.py +325 -0
- sahya_code/tools/plan/description.md +25 -0
- sahya_code/tools/plan/enter.py +183 -0
- sahya_code/tools/plan/enter_description.md +30 -0
- sahya_code/tools/plan/heroes.py +277 -0
- sahya_code/tools/shell/__init__.py +235 -0
- sahya_code/tools/shell/bash.md +35 -0
- sahya_code/tools/shell/powershell.md +30 -0
- sahya_code/tools/test.py +55 -0
- sahya_code/tools/think/__init__.py +21 -0
- sahya_code/tools/think/think.md +1 -0
- sahya_code/tools/todo/__init__.py +33 -0
- sahya_code/tools/todo/set_todo_list.md +15 -0
- sahya_code/tools/utils.py +199 -0
- sahya_code/tools/web/__init__.py +4 -0
- sahya_code/tools/web/fetch.md +1 -0
- sahya_code/tools/web/fetch.py +173 -0
- sahya_code/tools/web/search.md +1 -0
- sahya_code/tools/web/search.py +146 -0
- sahya_code/ui/__init__.py +0 -0
- sahya_code/ui/acp/__init__.py +99 -0
- sahya_code/ui/print/__init__.py +167 -0
- sahya_code/ui/print/visualize.py +185 -0
- sahya_code/ui/shell/__init__.py +991 -0
- sahya_code/ui/shell/approval_panel.py +481 -0
- sahya_code/ui/shell/console.py +105 -0
- sahya_code/ui/shell/debug.py +190 -0
- sahya_code/ui/shell/echo.py +17 -0
- sahya_code/ui/shell/export_import.py +111 -0
- sahya_code/ui/shell/keyboard.py +300 -0
- sahya_code/ui/shell/mcp_status.py +111 -0
- sahya_code/ui/shell/oauth.py +143 -0
- sahya_code/ui/shell/placeholders.py +530 -0
- sahya_code/ui/shell/prompt.py +2124 -0
- sahya_code/ui/shell/question_panel.py +586 -0
- sahya_code/ui/shell/replay.py +210 -0
- sahya_code/ui/shell/setup.py +212 -0
- sahya_code/ui/shell/slash.py +716 -0
- sahya_code/ui/shell/startup.py +32 -0
- sahya_code/ui/shell/task_browser.py +486 -0
- sahya_code/ui/shell/update.py +217 -0
- sahya_code/ui/shell/usage.py +281 -0
- sahya_code/ui/shell/visualize.py +1497 -0
- sahya_code/ui/theme.py +238 -0
- sahya_code/utils/__init__.py +0 -0
- sahya_code/utils/aiohttp.py +24 -0
- sahya_code/utils/aioqueue.py +72 -0
- sahya_code/utils/broadcast.py +37 -0
- sahya_code/utils/changelog.py +108 -0
- sahya_code/utils/clipboard.py +169 -0
- sahya_code/utils/datetime.py +37 -0
- sahya_code/utils/diff.py +135 -0
- sahya_code/utils/editor.py +91 -0
- sahya_code/utils/environment.py +58 -0
- sahya_code/utils/envvar.py +12 -0
- sahya_code/utils/export.py +696 -0
- sahya_code/utils/frontmatter.py +50 -0
- sahya_code/utils/io.py +27 -0
- sahya_code/utils/logging.py +124 -0
- sahya_code/utils/media_tags.py +29 -0
- sahya_code/utils/message.py +24 -0
- sahya_code/utils/path.py +140 -0
- sahya_code/utils/proctitle.py +33 -0
- sahya_code/utils/pyinstaller.py +32 -0
- sahya_code/utils/rich/__init__.py +33 -0
- sahya_code/utils/rich/columns.py +99 -0
- sahya_code/utils/rich/diff_render.py +436 -0
- sahya_code/utils/rich/markdown.py +900 -0
- sahya_code/utils/rich/markdown_sample.md +108 -0
- sahya_code/utils/rich/markdown_sample_short.md +2 -0
- sahya_code/utils/rich/syntax.py +114 -0
- sahya_code/utils/server.py +121 -0
- sahya_code/utils/signals.py +43 -0
- sahya_code/utils/slashcmd.py +124 -0
- sahya_code/utils/string.py +22 -0
- sahya_code/utils/subprocess_env.py +73 -0
- sahya_code/utils/term.py +168 -0
- sahya_code/utils/typing.py +20 -0
- sahya_code/vis/__init__.py +0 -0
- sahya_code/vis/api/__init__.py +5 -0
- sahya_code/vis/api/sessions.py +692 -0
- sahya_code/vis/api/statistics.py +209 -0
- sahya_code/vis/api/system.py +19 -0
- sahya_code/vis/app.py +175 -0
- sahya_code/web/__init__.py +5 -0
- sahya_code/web/api/__init__.py +15 -0
- sahya_code/web/api/config.py +208 -0
- sahya_code/web/api/open_in.py +197 -0
- sahya_code/web/api/sessions.py +1392 -0
- sahya_code/web/app.py +412 -0
- sahya_code/web/auth.py +191 -0
- sahya_code/web/models.py +98 -0
- sahya_code/web/runner/__init__.py +5 -0
- sahya_code/web/runner/messages.py +57 -0
- sahya_code/web/runner/process.py +745 -0
- sahya_code/web/runner/worker.py +87 -0
- sahya_code/web/store/__init__.py +1 -0
- sahya_code/web/store/sessions.py +517 -0
- sahya_code/wire/__init__.py +148 -0
- sahya_code/wire/file.py +151 -0
- sahya_code/wire/jsonrpc.py +263 -0
- sahya_code/wire/protocol.py +2 -0
- sahya_code/wire/root_hub.py +27 -0
- sahya_code/wire/serde.py +26 -0
- sahya_code/wire/server.py +1029 -0
- sahya_code/wire/types.py +674 -0
- sahya_code-1.0.0.dist-info/METADATA +158 -0
- sahya_code-1.0.0.dist-info/RECORD +239 -0
- sahya_code-1.0.0.dist-info/WHEEL +4 -0
- sahya_code-1.0.0.dist-info/entry_points.txt +4 -0
sahya_code/CHANGELOG.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Sahya Code will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-04-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release based on kimi-cli v1.28.0
|
|
12
|
+
- Custom LiteLLM endpoint support (https://llm.nexiant.ai)
|
|
13
|
+
- API key authentication via SAHYA_API_KEY environment variable
|
|
14
|
+
- Full rebranding from Kimi Code CLI to Sahya Code
|
|
15
|
+
- Pre-configured OpenAI-compatible provider for LiteLLM proxy
|
|
16
|
+
- Support for SAHYA_BASE_URL environment variable
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Package name:** `kimi-cli` → `sahya-code`
|
|
20
|
+
- **Module name:** `kimi_cli` → `sahya_code`
|
|
21
|
+
- **CLI command:** `kimi` → `sahya`
|
|
22
|
+
- **Config directory:** `~/.local/share/kimi` → `~/.local/share/sahya-code`
|
|
23
|
+
- **Log file:** `kimi.log` → `sahya.log`
|
|
24
|
+
- **Environment variable prefix:** `KIMI_*` → `SAHYA_*`
|
|
25
|
+
- **Main class:** `KimiCLI` → `SahyaCode`
|
|
26
|
+
- **Soul class:** `KimiSoul` → `SahyaSoul`
|
|
27
|
+
- **User agent:** `KimiCLI/*` → `SahyaCode/*`
|
|
28
|
+
- **Application name:** "Kimi Code CLI" → "Sahya Code"
|
|
29
|
+
|
|
30
|
+
### Configuration
|
|
31
|
+
|
|
32
|
+
#### Default Provider
|
|
33
|
+
- Type: `openai_legacy` (OpenAI-compatible API for LiteLLM)
|
|
34
|
+
- Endpoint: `https://llm.nexiant.ai`
|
|
35
|
+
- Default Model: `kimi-k2.5`
|
|
36
|
+
- Authentication: API key via `SAHYA_API_KEY`
|
|
37
|
+
|
|
38
|
+
#### Environment Variables
|
|
39
|
+
- `SAHYA_API_KEY` - API key for authentication (required)
|
|
40
|
+
- `SAHYA_BASE_URL` - Endpoint URL override (optional)
|
|
41
|
+
- `SAHYA_SHARE_DIR` - Custom share directory (optional)
|
|
42
|
+
- `SAHYA_CACHE_DIR` - Custom cache directory (optional)
|
|
43
|
+
|
|
44
|
+
#### Config File Location
|
|
45
|
+
- Default: `~/.local/share/sahya-code/config.toml`
|
|
46
|
+
- Format: TOML (JSON also supported)
|
|
47
|
+
|
|
48
|
+
### Removed
|
|
49
|
+
- Kimi-specific default configurations
|
|
50
|
+
- Moonshot AI-specific provider defaults
|
|
51
|
+
- Kimi-specific environment variable fallbacks
|
|
52
|
+
|
|
53
|
+
### Dependencies
|
|
54
|
+
Same as kimi-cli v1.28.0:
|
|
55
|
+
- Python >= 3.12
|
|
56
|
+
- kosong[contrib] == 0.47.0
|
|
57
|
+
- pydantic == 2.12.5
|
|
58
|
+
- typer == 0.21.1
|
|
59
|
+
- And other dependencies (see pyproject.toml)
|
|
60
|
+
|
|
61
|
+
## Original kimi-cli History
|
|
62
|
+
|
|
63
|
+
For complete history of the original project, see:
|
|
64
|
+
https://github.com/MoonshotAI/kimi-cli/blob/main/CHANGELOG.md
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Migration Guide
|
|
69
|
+
|
|
70
|
+
### From kimi-cli to sahya-code
|
|
71
|
+
|
|
72
|
+
1. **Uninstall kimi-cli:**
|
|
73
|
+
```bash
|
|
74
|
+
pip uninstall kimi-cli
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. **Install sahya-code:**
|
|
78
|
+
```bash
|
|
79
|
+
pip install sahya-code
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. **Update environment variables:**
|
|
83
|
+
```bash
|
|
84
|
+
# Old
|
|
85
|
+
export KIMI_API_KEY="..."
|
|
86
|
+
|
|
87
|
+
# New
|
|
88
|
+
export SAHYA_API_KEY="..."
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
4. **Migrate configuration:**
|
|
92
|
+
```bash
|
|
93
|
+
# Copy old config (optional)
|
|
94
|
+
mkdir -p ~/.local/share/sahya-code
|
|
95
|
+
cp ~/.local/share/kimi/config.toml ~/.local/share/sahya-code/config.toml
|
|
96
|
+
|
|
97
|
+
# Update config values
|
|
98
|
+
sed -i '' 's/kimi/sahya/g' ~/.local/share/sahya-code/config.toml
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
5. **Update aliases:**
|
|
102
|
+
```bash
|
|
103
|
+
# Old
|
|
104
|
+
alias ai='kimi'
|
|
105
|
+
|
|
106
|
+
# New
|
|
107
|
+
alias ai='sahya'
|
|
108
|
+
```
|
sahya_code/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _LazyLogger:
|
|
7
|
+
"""Import loguru only when logging is actually used."""
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._logger: Any | None = None
|
|
11
|
+
|
|
12
|
+
def _get(self) -> Any:
|
|
13
|
+
if self._logger is None:
|
|
14
|
+
from loguru import logger as real_logger
|
|
15
|
+
|
|
16
|
+
# Disable logging by default for library usage.
|
|
17
|
+
# Application entry points (e.g., sahya_code.cli) should call logger.enable("sahya_code")
|
|
18
|
+
# to enable logging.
|
|
19
|
+
real_logger.disable("sahya_code")
|
|
20
|
+
self._logger = real_logger
|
|
21
|
+
return self._logger
|
|
22
|
+
|
|
23
|
+
def __getattr__(self, name: str) -> Any:
|
|
24
|
+
return getattr(self._get(), name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = cast(Any, _LazyLogger())
|
|
28
|
+
|
|
29
|
+
__all__ = ["logger"]
|
sahya_code/__main__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _prog_name() -> str:
|
|
9
|
+
return Path(sys.argv[0]).name or "sahya"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: Sequence[str] | None = None) -> int | str | None:
|
|
13
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
14
|
+
|
|
15
|
+
if len(args) == 1 and args[0] in {"--version", "-V"}:
|
|
16
|
+
from sahya_code.constant import get_version
|
|
17
|
+
|
|
18
|
+
print(f"sahya, version {get_version()}")
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
from sahya_code.cli import cli
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
return cli(args=args, prog_name=_prog_name())
|
|
25
|
+
except SystemExit as exc:
|
|
26
|
+
return exc.code
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
raise SystemExit(main())
|
sahya_code/acp/AGENTS.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
- `ReadMediaFile` uses `KaosPath.read_bytes` to load image/video payloads through ACP reads.
|
|
86
|
+
- `WriteFile` uses `KaosPath.read_text/write_text/append_text` and still generates diffs
|
|
87
|
+
and approvals in the tool layer.
|
|
88
|
+
|
|
89
|
+
## Zed-specific notes (as of current integration)
|
|
90
|
+
- Zed does not currently call `authenticate`.
|
|
91
|
+
- Zed’s external agent server session management is not yet available, so
|
|
92
|
+
`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 sahya_code.acp.server import ACPServer
|
|
8
|
+
from sahya_code.app import enable_logging
|
|
9
|
+
from sahya_code.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,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import acp
|
|
4
|
+
|
|
5
|
+
from sahya_code.acp.types import ACPContentBlock
|
|
6
|
+
from sahya_code.utils.logging import logger
|
|
7
|
+
from sahya_code.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 acp.schema.EmbeddedResourceContentBlock():
|
|
32
|
+
resource = block.resource
|
|
33
|
+
if isinstance(resource, acp.schema.TextResourceContents):
|
|
34
|
+
uri = resource.uri
|
|
35
|
+
text = resource.text
|
|
36
|
+
content.append(TextPart(text=f"<resource uri={uri!r}>\n{text}\n</resource>"))
|
|
37
|
+
else:
|
|
38
|
+
logger.warning(
|
|
39
|
+
"Unsupported embedded resource type: {type}",
|
|
40
|
+
type=type(resource).__name__,
|
|
41
|
+
)
|
|
42
|
+
case acp.schema.ResourceContentBlock():
|
|
43
|
+
# ResourceContentBlock is a link reference without inline content;
|
|
44
|
+
# include the URI so the model is at least aware of the reference.
|
|
45
|
+
content.append(
|
|
46
|
+
TextPart(text=f"<resource_link uri={block.uri!r} name={block.name!r} />")
|
|
47
|
+
)
|
|
48
|
+
case _:
|
|
49
|
+
logger.warning("Unsupported prompt content block: {block}", block=block)
|
|
50
|
+
return content
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def display_block_to_acp_content(
|
|
54
|
+
block: DisplayBlock,
|
|
55
|
+
) -> acp.schema.FileEditToolCallContent | None:
|
|
56
|
+
if isinstance(block, DiffDisplayBlock):
|
|
57
|
+
return acp.schema.FileEditToolCallContent(
|
|
58
|
+
type="diff",
|
|
59
|
+
path=block.path,
|
|
60
|
+
old_text=block.old_text,
|
|
61
|
+
new_text=block.new_text,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def tool_result_to_acp_content(
|
|
68
|
+
tool_ret: ToolReturnValue,
|
|
69
|
+
) -> list[
|
|
70
|
+
acp.schema.ContentToolCallContent
|
|
71
|
+
| acp.schema.FileEditToolCallContent
|
|
72
|
+
| acp.schema.TerminalToolCallContent
|
|
73
|
+
]:
|
|
74
|
+
from sahya_code.acp.tools import HideOutputDisplayBlock
|
|
75
|
+
|
|
76
|
+
def _to_acp_content(
|
|
77
|
+
part: ContentPart,
|
|
78
|
+
) -> (
|
|
79
|
+
acp.schema.ContentToolCallContent
|
|
80
|
+
| acp.schema.FileEditToolCallContent
|
|
81
|
+
| acp.schema.TerminalToolCallContent
|
|
82
|
+
):
|
|
83
|
+
if isinstance(part, TextPart):
|
|
84
|
+
return acp.schema.ContentToolCallContent(
|
|
85
|
+
type="content", content=acp.schema.TextContentBlock(type="text", text=part.text)
|
|
86
|
+
)
|
|
87
|
+
logger.warning("Unsupported content part in tool result: {part}", part=part)
|
|
88
|
+
return acp.schema.ContentToolCallContent(
|
|
89
|
+
type="content",
|
|
90
|
+
content=acp.schema.TextContentBlock(type="text", text=f"[{part.__class__.__name__}]"),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _to_text_block(text: str) -> acp.schema.ContentToolCallContent:
|
|
94
|
+
return acp.schema.ContentToolCallContent(
|
|
95
|
+
type="content", content=acp.schema.TextContentBlock(type="text", text=text)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
contents: list[
|
|
99
|
+
acp.schema.ContentToolCallContent
|
|
100
|
+
| acp.schema.FileEditToolCallContent
|
|
101
|
+
| acp.schema.TerminalToolCallContent
|
|
102
|
+
] = []
|
|
103
|
+
|
|
104
|
+
for block in tool_ret.display:
|
|
105
|
+
if isinstance(block, HideOutputDisplayBlock):
|
|
106
|
+
# return early to indicate no output should be shown
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
content = display_block_to_acp_content(block)
|
|
110
|
+
if content is not None:
|
|
111
|
+
contents.append(content)
|
|
112
|
+
# TODO: better concatenation of `display` blocks and `output`?
|
|
113
|
+
|
|
114
|
+
output = tool_ret.output
|
|
115
|
+
if isinstance(output, str):
|
|
116
|
+
if output:
|
|
117
|
+
contents.append(_to_text_block(output))
|
|
118
|
+
else:
|
|
119
|
+
# NOTE: At the moment, ToolReturnValue.output is either a string or a
|
|
120
|
+
# list of ContentPart. We avoid an unnecessary isinstance() check here
|
|
121
|
+
# to keep pyright happy while still handling list outputs.
|
|
122
|
+
contents.extend(_to_acp_content(part) for part in output)
|
|
123
|
+
|
|
124
|
+
if not contents and tool_ret.message:
|
|
125
|
+
# Fallback to the `message` for LLM if there's no other content
|
|
126
|
+
contents.append(_to_text_block(tool_ret.message))
|
|
127
|
+
|
|
128
|
+
return contents
|
sahya_code/acp/kaos.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncGenerator, Iterable, Mapping
|
|
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
|
+
client: acp.Client,
|
|
50
|
+
session_id: str,
|
|
51
|
+
terminal_id: str,
|
|
52
|
+
*,
|
|
53
|
+
poll_interval: float = _DEFAULT_POLL_INTERVAL,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._client = client
|
|
56
|
+
self._session_id = session_id
|
|
57
|
+
self._terminal_id = terminal_id
|
|
58
|
+
self._poll_interval = poll_interval
|
|
59
|
+
self._stdin = _NullWritable()
|
|
60
|
+
self._stdout = asyncio.StreamReader()
|
|
61
|
+
self._stderr = asyncio.StreamReader()
|
|
62
|
+
self.stdin: AsyncWritable = self._stdin
|
|
63
|
+
self.stdout: AsyncReadable = self._stdout
|
|
64
|
+
# ACP does not expose stderr separately; keep stderr empty.
|
|
65
|
+
self.stderr: AsyncReadable = self._stderr
|
|
66
|
+
self._returncode: int | None = None
|
|
67
|
+
self._last_output = ""
|
|
68
|
+
self._truncation_noted = False
|
|
69
|
+
self._exit_future: asyncio.Future[int] = asyncio.get_running_loop().create_future()
|
|
70
|
+
self._poll_task = asyncio.create_task(self._poll_output())
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def pid(self) -> int:
|
|
74
|
+
return -1
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def returncode(self) -> int | None:
|
|
78
|
+
return self._returncode
|
|
79
|
+
|
|
80
|
+
async def wait(self) -> int:
|
|
81
|
+
return await self._exit_future
|
|
82
|
+
|
|
83
|
+
async def kill(self) -> None:
|
|
84
|
+
await self._client.kill_terminal(
|
|
85
|
+
session_id=self._session_id,
|
|
86
|
+
terminal_id=self._terminal_id,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _feed_output(self, output_response: acp.schema.TerminalOutputResponse) -> None:
|
|
90
|
+
output = output_response.output
|
|
91
|
+
reset = output_response.truncated or (
|
|
92
|
+
self._last_output and not output.startswith(self._last_output)
|
|
93
|
+
)
|
|
94
|
+
if reset and self._last_output and not self._truncation_noted:
|
|
95
|
+
self._stdout.feed_data(_TRUNCATION_NOTICE.encode("utf-8"))
|
|
96
|
+
self._truncation_noted = True
|
|
97
|
+
|
|
98
|
+
delta = output if reset else output[len(self._last_output) :]
|
|
99
|
+
if delta:
|
|
100
|
+
self._stdout.feed_data(delta.encode("utf-8", "replace"))
|
|
101
|
+
self._last_output = output
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _normalize_exit_code(exit_code: int | None) -> int:
|
|
105
|
+
return 1 if exit_code is None else exit_code
|
|
106
|
+
|
|
107
|
+
async def _poll_output(self) -> None:
|
|
108
|
+
exit_task = asyncio.create_task(
|
|
109
|
+
self._client.wait_for_terminal_exit(
|
|
110
|
+
session_id=self._session_id,
|
|
111
|
+
terminal_id=self._terminal_id,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
exit_code: int | None = None
|
|
115
|
+
try:
|
|
116
|
+
while True:
|
|
117
|
+
if exit_task.done():
|
|
118
|
+
exit_response = exit_task.result()
|
|
119
|
+
exit_code = exit_response.exit_code
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
output_response = await self._client.terminal_output(
|
|
123
|
+
session_id=self._session_id,
|
|
124
|
+
terminal_id=self._terminal_id,
|
|
125
|
+
)
|
|
126
|
+
self._feed_output(output_response)
|
|
127
|
+
if output_response.exit_status:
|
|
128
|
+
exit_code = output_response.exit_status.exit_code
|
|
129
|
+
try:
|
|
130
|
+
exit_response = await exit_task
|
|
131
|
+
exit_code = exit_response.exit_code or exit_code
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
await asyncio.sleep(self._poll_interval)
|
|
137
|
+
|
|
138
|
+
final_output = await self._client.terminal_output(
|
|
139
|
+
session_id=self._session_id,
|
|
140
|
+
terminal_id=self._terminal_id,
|
|
141
|
+
)
|
|
142
|
+
self._feed_output(final_output)
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
error_note = f"[acp terminal error] {exc}\n"
|
|
145
|
+
self._stdout.feed_data(error_note.encode("utf-8", "replace"))
|
|
146
|
+
if exit_code is None:
|
|
147
|
+
exit_code = 1
|
|
148
|
+
finally:
|
|
149
|
+
if not exit_task.done():
|
|
150
|
+
exit_task.cancel()
|
|
151
|
+
with suppress(Exception):
|
|
152
|
+
await exit_task
|
|
153
|
+
self._returncode = self._normalize_exit_code(exit_code)
|
|
154
|
+
self._stdout.feed_eof()
|
|
155
|
+
self._stderr.feed_eof()
|
|
156
|
+
if not self._exit_future.done():
|
|
157
|
+
self._exit_future.set_result(self._returncode)
|
|
158
|
+
with suppress(Exception):
|
|
159
|
+
await self._client.release_terminal(
|
|
160
|
+
session_id=self._session_id,
|
|
161
|
+
terminal_id=self._terminal_id,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ACPKaos:
|
|
166
|
+
"""KAOS backend that routes supported operations through ACP."""
|
|
167
|
+
|
|
168
|
+
name: str = "acp"
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
client: acp.Client,
|
|
173
|
+
session_id: str,
|
|
174
|
+
client_capabilities: acp.schema.ClientCapabilities | None,
|
|
175
|
+
fallback: Kaos | None = None,
|
|
176
|
+
*,
|
|
177
|
+
output_byte_limit: int | None = _DEFAULT_TERMINAL_OUTPUT_LIMIT,
|
|
178
|
+
poll_interval: float = _DEFAULT_POLL_INTERVAL,
|
|
179
|
+
) -> None:
|
|
180
|
+
self._client = client
|
|
181
|
+
self._session_id = session_id
|
|
182
|
+
self._fallback = fallback or local_kaos
|
|
183
|
+
fs = client_capabilities.fs if client_capabilities else None
|
|
184
|
+
self._supports_read = bool(fs and fs.read_text_file)
|
|
185
|
+
self._supports_write = bool(fs and fs.write_text_file)
|
|
186
|
+
self._supports_terminal = bool(client_capabilities and client_capabilities.terminal)
|
|
187
|
+
self._output_byte_limit = output_byte_limit
|
|
188
|
+
self._poll_interval = poll_interval
|
|
189
|
+
|
|
190
|
+
def pathclass(self):
|
|
191
|
+
return self._fallback.pathclass()
|
|
192
|
+
|
|
193
|
+
def normpath(self, path: StrOrKaosPath) -> KaosPath:
|
|
194
|
+
return self._fallback.normpath(path)
|
|
195
|
+
|
|
196
|
+
def gethome(self) -> KaosPath:
|
|
197
|
+
return self._fallback.gethome()
|
|
198
|
+
|
|
199
|
+
def getcwd(self) -> KaosPath:
|
|
200
|
+
return self._fallback.getcwd()
|
|
201
|
+
|
|
202
|
+
async def chdir(self, path: StrOrKaosPath) -> None:
|
|
203
|
+
await self._fallback.chdir(path)
|
|
204
|
+
|
|
205
|
+
async def stat(self, path: StrOrKaosPath, *, follow_symlinks: bool = True) -> StatResult:
|
|
206
|
+
return await self._fallback.stat(path, follow_symlinks=follow_symlinks)
|
|
207
|
+
|
|
208
|
+
def iterdir(self, path: StrOrKaosPath) -> AsyncGenerator[KaosPath]:
|
|
209
|
+
return self._fallback.iterdir(path)
|
|
210
|
+
|
|
211
|
+
def glob(
|
|
212
|
+
self, path: StrOrKaosPath, pattern: str, *, case_sensitive: bool = True
|
|
213
|
+
) -> AsyncGenerator[KaosPath]:
|
|
214
|
+
return self._fallback.glob(path, pattern, case_sensitive=case_sensitive)
|
|
215
|
+
|
|
216
|
+
async def readbytes(self, path: StrOrKaosPath, n: int | None = None) -> bytes:
|
|
217
|
+
return await self._fallback.readbytes(path, n=n)
|
|
218
|
+
|
|
219
|
+
async def readtext(
|
|
220
|
+
self,
|
|
221
|
+
path: StrOrKaosPath,
|
|
222
|
+
*,
|
|
223
|
+
encoding: str = "utf-8",
|
|
224
|
+
errors: Literal["strict", "ignore", "replace"] = "strict",
|
|
225
|
+
) -> str:
|
|
226
|
+
abs_path = self._abs_path(path)
|
|
227
|
+
if not self._supports_read:
|
|
228
|
+
return await self._fallback.readtext(abs_path, encoding=encoding, errors=errors)
|
|
229
|
+
response = await self._client.read_text_file(path=abs_path, session_id=self._session_id)
|
|
230
|
+
return response.content
|
|
231
|
+
|
|
232
|
+
async def readlines(
|
|
233
|
+
self,
|
|
234
|
+
path: StrOrKaosPath,
|
|
235
|
+
*,
|
|
236
|
+
encoding: str = "utf-8",
|
|
237
|
+
errors: Literal["strict", "ignore", "replace"] = "strict",
|
|
238
|
+
) -> AsyncGenerator[str]:
|
|
239
|
+
text = await self.readtext(path, encoding=encoding, errors=errors)
|
|
240
|
+
for line in text.splitlines(keepends=True):
|
|
241
|
+
yield line
|
|
242
|
+
|
|
243
|
+
async def writebytes(self, path: StrOrKaosPath, data: bytes) -> int:
|
|
244
|
+
return await self._fallback.writebytes(path, data)
|
|
245
|
+
|
|
246
|
+
async def writetext(
|
|
247
|
+
self,
|
|
248
|
+
path: StrOrKaosPath,
|
|
249
|
+
data: str,
|
|
250
|
+
*,
|
|
251
|
+
mode: Literal["w", "a"] = "w",
|
|
252
|
+
encoding: str = "utf-8",
|
|
253
|
+
errors: Literal["strict", "ignore", "replace"] = "strict",
|
|
254
|
+
) -> int:
|
|
255
|
+
abs_path = self._abs_path(path)
|
|
256
|
+
if mode == "a":
|
|
257
|
+
if self._supports_read and self._supports_write:
|
|
258
|
+
existing = await self.readtext(abs_path, encoding=encoding, errors=errors)
|
|
259
|
+
await self._client.write_text_file(
|
|
260
|
+
path=abs_path,
|
|
261
|
+
content=existing + data,
|
|
262
|
+
session_id=self._session_id,
|
|
263
|
+
)
|
|
264
|
+
return len(data)
|
|
265
|
+
return await self._fallback.writetext(
|
|
266
|
+
abs_path, data, mode="a", encoding=encoding, errors=errors
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if not self._supports_write:
|
|
270
|
+
return await self._fallback.writetext(
|
|
271
|
+
abs_path, data, mode=mode, encoding=encoding, errors=errors
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
await self._client.write_text_file(
|
|
275
|
+
path=abs_path,
|
|
276
|
+
content=data,
|
|
277
|
+
session_id=self._session_id,
|
|
278
|
+
)
|
|
279
|
+
return len(data)
|
|
280
|
+
|
|
281
|
+
async def mkdir(
|
|
282
|
+
self, path: StrOrKaosPath, parents: bool = False, exist_ok: bool = False
|
|
283
|
+
) -> None:
|
|
284
|
+
await self._fallback.mkdir(path, parents=parents, exist_ok=exist_ok)
|
|
285
|
+
|
|
286
|
+
async def exec(self, *args: str, env: Mapping[str, str] | None = None) -> KaosProcess:
|
|
287
|
+
return await self._fallback.exec(*args, env=env)
|
|
288
|
+
|
|
289
|
+
def _abs_path(self, path: StrOrKaosPath) -> str:
|
|
290
|
+
kaos_path = path if isinstance(path, KaosPath) else KaosPath(path)
|
|
291
|
+
return str(kaos_path.canonical())
|