kiwimatecoder 0.1.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.
- kiwimatecoder/__init__.py +8 -0
- kiwimatecoder/__main__.py +12 -0
- kiwimatecoder/acp/__init__.py +13 -0
- kiwimatecoder/acp/protocol.py +115 -0
- kiwimatecoder/acp/server.py +823 -0
- kiwimatecoder/acp/stdio.py +32 -0
- kiwimatecoder/agent.py +1162 -0
- kiwimatecoder/ai.py +68 -0
- kiwimatecoder/audit.py +65 -0
- kiwimatecoder/browser.py +261 -0
- kiwimatecoder/catalog.py +445 -0
- kiwimatecoder/checkpoints.py +103 -0
- kiwimatecoder/client.py +820 -0
- kiwimatecoder/clipboard.py +99 -0
- kiwimatecoder/commands.py +6757 -0
- kiwimatecoder/config.py +5335 -0
- kiwimatecoder/diagnostics.py +295 -0
- kiwimatecoder/documents.py +299 -0
- kiwimatecoder/evals/__init__.py +39 -0
- kiwimatecoder/evals/cases.py +230 -0
- kiwimatecoder/evals/render.py +46 -0
- kiwimatecoder/evals/runner.py +319 -0
- kiwimatecoder/events.py +95 -0
- kiwimatecoder/forge.py +172 -0
- kiwimatecoder/git.py +164 -0
- kiwimatecoder/headless.py +255 -0
- kiwimatecoder/hooks.py +170 -0
- kiwimatecoder/hunks.py +260 -0
- kiwimatecoder/i18n.py +368 -0
- kiwimatecoder/images.py +195 -0
- kiwimatecoder/index/__init__.py +14 -0
- kiwimatecoder/index/builder.py +376 -0
- kiwimatecoder/index/embeddings.py +151 -0
- kiwimatecoder/index/search.py +247 -0
- kiwimatecoder/index/store.py +251 -0
- kiwimatecoder/instructions.py +83 -0
- kiwimatecoder/jobs.py +594 -0
- kiwimatecoder/lsp/__init__.py +56 -0
- kiwimatecoder/lsp/client.py +548 -0
- kiwimatecoder/lsp/manager.py +353 -0
- kiwimatecoder/lsp/protocol.py +79 -0
- kiwimatecoder/main.py +4258 -0
- kiwimatecoder/mcp/__init__.py +45 -0
- kiwimatecoder/mcp/client.py +385 -0
- kiwimatecoder/mcp/errors.py +7 -0
- kiwimatecoder/mcp/manager.py +253 -0
- kiwimatecoder/mcp/protocol.py +132 -0
- kiwimatecoder/media.py +904 -0
- kiwimatecoder/memory.py +141 -0
- kiwimatecoder/network.py +112 -0
- kiwimatecoder/notify.py +139 -0
- kiwimatecoder/permissions.py +171 -0
- kiwimatecoder/pickers.py +417 -0
- kiwimatecoder/plugins.py +245 -0
- kiwimatecoder/pricing.py +167 -0
- kiwimatecoder/prompts.py +213 -0
- kiwimatecoder/providers.py +342 -0
- kiwimatecoder/redaction.py +66 -0
- kiwimatecoder/remote.py +294 -0
- kiwimatecoder/repl.py +1388 -0
- kiwimatecoder/routing.py +65 -0
- kiwimatecoder/sandbox.py +169 -0
- kiwimatecoder/sdk.py +146 -0
- kiwimatecoder/session.py +574 -0
- kiwimatecoder/share.py +386 -0
- kiwimatecoder/shell.py +660 -0
- kiwimatecoder/skills.py +138 -0
- kiwimatecoder/sync.py +513 -0
- kiwimatecoder/team.py +249 -0
- kiwimatecoder/telemetry.py +392 -0
- kiwimatecoder/templates.py +108 -0
- kiwimatecoder/tools/__init__.py +190 -0
- kiwimatecoder/tools/ask.py +62 -0
- kiwimatecoder/tools/base.py +69 -0
- kiwimatecoder/tools/browser.py +261 -0
- kiwimatecoder/tools/edit_file.py +143 -0
- kiwimatecoder/tools/forge.py +207 -0
- kiwimatecoder/tools/git.py +158 -0
- kiwimatecoder/tools/image.py +300 -0
- kiwimatecoder/tools/io.py +346 -0
- kiwimatecoder/tools/list_dir.py +67 -0
- kiwimatecoder/tools/lsp.py +254 -0
- kiwimatecoder/tools/memory.py +115 -0
- kiwimatecoder/tools/paths.py +234 -0
- kiwimatecoder/tools/read_file.py +113 -0
- kiwimatecoder/tools/registry.py +91 -0
- kiwimatecoder/tools/run_bash.py +189 -0
- kiwimatecoder/tools/search.py +224 -0
- kiwimatecoder/tools/selection.py +108 -0
- kiwimatecoder/tools/shell.py +158 -0
- kiwimatecoder/tools/skill.py +46 -0
- kiwimatecoder/tools/task.py +61 -0
- kiwimatecoder/tools/todo.py +77 -0
- kiwimatecoder/tools/web.py +146 -0
- kiwimatecoder/tools/write_file.py +83 -0
- kiwimatecoder/ui.py +189 -0
- kiwimatecoder/updater.py +502 -0
- kiwimatecoder/web.py +499 -0
- kiwimatecoder-0.1.0.dist-info/METADATA +2016 -0
- kiwimatecoder-0.1.0.dist-info/RECORD +104 -0
- kiwimatecoder-0.1.0.dist-info/WHEEL +5 -0
- kiwimatecoder-0.1.0.dist-info/entry_points.txt +2 -0
- kiwimatecoder-0.1.0.dist-info/licenses/LICENSE +21 -0
- kiwimatecoder-0.1.0.dist-info/top_level.txt +1 -0
kiwimatecoder/remote.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Remote (SSH) and devcontainer command execution.
|
|
2
|
+
|
|
3
|
+
This module is pure command construction: it decides *where* a shell command
|
|
4
|
+
should run (a remote host over SSH, inside a devcontainer, or in a Docker
|
|
5
|
+
container) but never runs anything itself. That keeps it trivially testable and
|
|
6
|
+
lets callers fall back to local execution when the required CLI is missing.
|
|
7
|
+
|
|
8
|
+
Order of preference when ``remote.devcontainer`` is ``"auto"``:
|
|
9
|
+
|
|
10
|
+
1. a detected ``.devcontainer/devcontainer.json`` plus the ``devcontainer`` CLI,
|
|
11
|
+
2. the detected container through ``docker exec``,
|
|
12
|
+
3. SSH when ``remote.host`` is configured,
|
|
13
|
+
4. local execution (the default) with a warning.
|
|
14
|
+
|
|
15
|
+
Only the *command* runs remotely. File tools still read and write the locally
|
|
16
|
+
accessible workspace, so point them at an sshfs/bind mount of the remote tree
|
|
17
|
+
(or accept that edits land locally). See the README section "Remote and
|
|
18
|
+
devcontainer execution" for the full limitations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import shlex
|
|
25
|
+
import shutil
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from kiwimatecoder.config import get_remote
|
|
31
|
+
|
|
32
|
+
SSH_WARNING = (
|
|
33
|
+
"Remote execution is enabled but the 'ssh' CLI was not found; "
|
|
34
|
+
"running the command locally."
|
|
35
|
+
)
|
|
36
|
+
DOCKER_WARNING = (
|
|
37
|
+
"Remote execution is enabled but the 'docker' CLI was not found; "
|
|
38
|
+
"running the command locally."
|
|
39
|
+
)
|
|
40
|
+
DEVCONTAINER_WARNING = (
|
|
41
|
+
"Remote execution is enabled but neither the 'devcontainer' nor the "
|
|
42
|
+
"'docker' CLI was found; running the command locally."
|
|
43
|
+
)
|
|
44
|
+
NO_TARGET_WARNING = (
|
|
45
|
+
"Remote execution is enabled but no host or container is configured; "
|
|
46
|
+
"running the command locally."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
DEVCONTAINER_CONFIG = Path(".devcontainer") / "devcontainer.json"
|
|
50
|
+
DEFAULT_PORT = 22
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class RemoteTarget:
|
|
55
|
+
"""Where remote commands should run, built from the ``remote`` config."""
|
|
56
|
+
|
|
57
|
+
enabled: bool = False
|
|
58
|
+
host: str = ""
|
|
59
|
+
user: str = ""
|
|
60
|
+
port: int = DEFAULT_PORT
|
|
61
|
+
identity: str = ""
|
|
62
|
+
workspace: str = ""
|
|
63
|
+
devcontainer: str = "auto"
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_config(cls, cfg: dict[str, Any] | None = None) -> "RemoteTarget":
|
|
67
|
+
"""Build a target from the active (or given) configuration."""
|
|
68
|
+
return cls(**get_remote(cfg))
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def address(self) -> str:
|
|
72
|
+
"""``user@host`` when a user is set, else just the host."""
|
|
73
|
+
return f"{self.user}@{self.host}" if self.user else self.host
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class DevcontainerInfo:
|
|
78
|
+
"""The parts of ``.devcontainer/devcontainer.json`` this module uses."""
|
|
79
|
+
|
|
80
|
+
name: str
|
|
81
|
+
workspace_folder: str
|
|
82
|
+
config_path: Path
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def remote_enabled(cfg: dict[str, Any] | None = None) -> bool:
|
|
86
|
+
"""Whether remote/devcontainer execution is turned on."""
|
|
87
|
+
return bool(get_remote(cfg)["enabled"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _strip_line_comments(text: str) -> str:
|
|
91
|
+
"""Drop full-line ``//`` comments so devcontainer JSON parses."""
|
|
92
|
+
lines = [
|
|
93
|
+
line for line in text.splitlines() if not line.lstrip().startswith("//")
|
|
94
|
+
]
|
|
95
|
+
return "\n".join(lines)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def detect_devcontainer(workspace: str | Path) -> DevcontainerInfo | None:
|
|
99
|
+
"""Read ``.devcontainer/devcontainer.json`` under ``workspace``, if valid.
|
|
100
|
+
|
|
101
|
+
Returns ``None`` when the file is missing, unreadable, or malformed. The
|
|
102
|
+
container name is ``name`` then ``service`` from the file, falling back to
|
|
103
|
+
the workspace directory's basename; the workspace folder is
|
|
104
|
+
``workspaceFolder`` when set, else ``/workspaces/<basename>``.
|
|
105
|
+
"""
|
|
106
|
+
root = Path(workspace).expanduser()
|
|
107
|
+
path = root / DEVCONTAINER_CONFIG
|
|
108
|
+
if not path.is_file():
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
text = path.read_text(encoding="utf-8")
|
|
112
|
+
except OSError:
|
|
113
|
+
return None
|
|
114
|
+
try:
|
|
115
|
+
data = json.loads(_strip_line_comments(text))
|
|
116
|
+
except (json.JSONDecodeError, ValueError):
|
|
117
|
+
return None
|
|
118
|
+
if not isinstance(data, dict):
|
|
119
|
+
return None
|
|
120
|
+
basename = root.name or "workspace"
|
|
121
|
+
container = ""
|
|
122
|
+
for key in ("name", "service"):
|
|
123
|
+
value = data.get(key)
|
|
124
|
+
if isinstance(value, str) and value.strip():
|
|
125
|
+
container = value.strip()
|
|
126
|
+
break
|
|
127
|
+
if not container:
|
|
128
|
+
container = basename
|
|
129
|
+
folder = data.get("workspaceFolder")
|
|
130
|
+
workspace_folder = (
|
|
131
|
+
folder.strip()
|
|
132
|
+
if isinstance(folder, str) and folder.strip()
|
|
133
|
+
else f"/workspaces/{basename}"
|
|
134
|
+
)
|
|
135
|
+
return DevcontainerInfo(
|
|
136
|
+
name=container, workspace_folder=workspace_folder, config_path=path
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _remote_command(command: str, workspace: str = "") -> str:
|
|
141
|
+
"""Prefix ``command`` with a ``cd`` when a remote workspace is set."""
|
|
142
|
+
if workspace:
|
|
143
|
+
return f"cd {shlex.quote(workspace)} && {command}"
|
|
144
|
+
return command
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def ssh_argv(command: str, target: RemoteTarget) -> list[str]:
|
|
148
|
+
"""Build the SSH invocation for ``command``.
|
|
149
|
+
|
|
150
|
+
The remote command is quoted for the SSH wire: the client joins the remote
|
|
151
|
+
argv with spaces and hands the result to the remote login shell, so an
|
|
152
|
+
unquoted ``sh -lc`` argument would be split apart.
|
|
153
|
+
"""
|
|
154
|
+
argv = ["ssh", "-o", "BatchMode=yes"]
|
|
155
|
+
if target.port and target.port != DEFAULT_PORT:
|
|
156
|
+
argv += ["-p", str(target.port)]
|
|
157
|
+
if target.identity:
|
|
158
|
+
argv += ["-i", target.identity]
|
|
159
|
+
remote = _remote_command(command, target.workspace)
|
|
160
|
+
argv += [target.address, "--", "sh", "-lc", shlex.quote(remote)]
|
|
161
|
+
return argv
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def docker_argv(
|
|
165
|
+
command: str,
|
|
166
|
+
container: str,
|
|
167
|
+
workspace: str = "",
|
|
168
|
+
*,
|
|
169
|
+
interactive: bool = False,
|
|
170
|
+
) -> list[str]:
|
|
171
|
+
"""Build ``docker exec`` for ``command`` in ``container``.
|
|
172
|
+
|
|
173
|
+
``interactive`` adds ``-i`` so a persistent shell can read commands from
|
|
174
|
+
stdin; one-shot commands do not need it.
|
|
175
|
+
"""
|
|
176
|
+
argv = ["docker", "exec"]
|
|
177
|
+
if interactive:
|
|
178
|
+
argv.append("-i")
|
|
179
|
+
if workspace:
|
|
180
|
+
argv += ["-w", workspace]
|
|
181
|
+
argv += [container, "sh", "-lc", command]
|
|
182
|
+
return argv
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def devcontainer_argv(command: str, workspace: str | Path) -> list[str]:
|
|
186
|
+
"""Build ``devcontainer exec --workspace-folder <workspace> ...``."""
|
|
187
|
+
return [
|
|
188
|
+
"devcontainer",
|
|
189
|
+
"exec",
|
|
190
|
+
"--workspace-folder",
|
|
191
|
+
str(workspace),
|
|
192
|
+
"sh",
|
|
193
|
+
"-lc",
|
|
194
|
+
command,
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _resolve_remote(
|
|
199
|
+
command: str,
|
|
200
|
+
*,
|
|
201
|
+
workspace: str | Path,
|
|
202
|
+
cfg: dict[str, Any] | None = None,
|
|
203
|
+
interactive: bool = False,
|
|
204
|
+
) -> tuple[list[str] | None, str | None, str | None]:
|
|
205
|
+
"""Return ``(argv, warning, label)`` for the best remote route.
|
|
206
|
+
|
|
207
|
+
``argv`` is ``None`` when the command should run locally; ``warning`` is
|
|
208
|
+
then set when remote execution was requested but could not be arranged.
|
|
209
|
+
"""
|
|
210
|
+
target = RemoteTarget.from_config(cfg)
|
|
211
|
+
if not target.enabled:
|
|
212
|
+
return None, None, None
|
|
213
|
+
root = Path(workspace).expanduser()
|
|
214
|
+
info = detect_devcontainer(root) if target.devcontainer == "auto" else None
|
|
215
|
+
|
|
216
|
+
if target.devcontainer not in {"auto", "off"}:
|
|
217
|
+
if shutil.which("docker"):
|
|
218
|
+
label = f"docker exec {target.devcontainer}"
|
|
219
|
+
if target.workspace:
|
|
220
|
+
label += f" (workspace {target.workspace})"
|
|
221
|
+
return (
|
|
222
|
+
docker_argv(
|
|
223
|
+
command,
|
|
224
|
+
target.devcontainer,
|
|
225
|
+
target.workspace,
|
|
226
|
+
interactive=interactive,
|
|
227
|
+
),
|
|
228
|
+
None,
|
|
229
|
+
label,
|
|
230
|
+
)
|
|
231
|
+
return None, DOCKER_WARNING, None
|
|
232
|
+
|
|
233
|
+
if info is not None:
|
|
234
|
+
if shutil.which("devcontainer"):
|
|
235
|
+
return (
|
|
236
|
+
devcontainer_argv(command, root),
|
|
237
|
+
None,
|
|
238
|
+
f"devcontainer {info.name} (folder {root})",
|
|
239
|
+
)
|
|
240
|
+
if shutil.which("docker"):
|
|
241
|
+
return (
|
|
242
|
+
docker_argv(
|
|
243
|
+
command,
|
|
244
|
+
info.name,
|
|
245
|
+
info.workspace_folder,
|
|
246
|
+
interactive=interactive,
|
|
247
|
+
),
|
|
248
|
+
None,
|
|
249
|
+
f"docker exec {info.name} (workspace {info.workspace_folder})",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if target.host:
|
|
253
|
+
if shutil.which("ssh"):
|
|
254
|
+
label = f"ssh {target.address}"
|
|
255
|
+
if target.workspace:
|
|
256
|
+
label += f" (cd {target.workspace})"
|
|
257
|
+
return ssh_argv(command, target), None, label
|
|
258
|
+
return None, SSH_WARNING, None
|
|
259
|
+
|
|
260
|
+
if info is not None:
|
|
261
|
+
return None, DEVCONTAINER_WARNING, None
|
|
262
|
+
return None, NO_TARGET_WARNING, None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def wrap_remote_command(
|
|
266
|
+
command: str,
|
|
267
|
+
*,
|
|
268
|
+
workspace: str | Path,
|
|
269
|
+
cfg: dict[str, Any] | None = None,
|
|
270
|
+
interactive: bool = False,
|
|
271
|
+
) -> tuple[list[str] | None, str | None]:
|
|
272
|
+
"""Return the remote argv to execute plus an optional warning.
|
|
273
|
+
|
|
274
|
+
``(None, None)`` means the caller should run the command locally with its
|
|
275
|
+
normal shell (remote execution is off). ``(None, warning)`` means remote
|
|
276
|
+
execution was requested but could not be arranged and the caller should
|
|
277
|
+
surface the warning before falling back to local execution.
|
|
278
|
+
"""
|
|
279
|
+
argv, warning, _label = _resolve_remote(
|
|
280
|
+
command, workspace=workspace, cfg=cfg, interactive=interactive
|
|
281
|
+
)
|
|
282
|
+
return argv, warning
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def remote_preview(
|
|
286
|
+
command: str, *, workspace: str | Path, cfg: dict[str, Any] | None = None
|
|
287
|
+
) -> str:
|
|
288
|
+
"""Return command text plus a readable remote note for approval prompts."""
|
|
289
|
+
argv, warning, label = _resolve_remote(command, workspace=workspace, cfg=cfg)
|
|
290
|
+
if argv is None:
|
|
291
|
+
if warning:
|
|
292
|
+
return f"{command}\n[remote] {warning}"
|
|
293
|
+
return command
|
|
294
|
+
return f"{command}\n[remote] via {label}"
|