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.
Files changed (104) hide show
  1. kiwimatecoder/__init__.py +8 -0
  2. kiwimatecoder/__main__.py +12 -0
  3. kiwimatecoder/acp/__init__.py +13 -0
  4. kiwimatecoder/acp/protocol.py +115 -0
  5. kiwimatecoder/acp/server.py +823 -0
  6. kiwimatecoder/acp/stdio.py +32 -0
  7. kiwimatecoder/agent.py +1162 -0
  8. kiwimatecoder/ai.py +68 -0
  9. kiwimatecoder/audit.py +65 -0
  10. kiwimatecoder/browser.py +261 -0
  11. kiwimatecoder/catalog.py +445 -0
  12. kiwimatecoder/checkpoints.py +103 -0
  13. kiwimatecoder/client.py +820 -0
  14. kiwimatecoder/clipboard.py +99 -0
  15. kiwimatecoder/commands.py +6757 -0
  16. kiwimatecoder/config.py +5335 -0
  17. kiwimatecoder/diagnostics.py +295 -0
  18. kiwimatecoder/documents.py +299 -0
  19. kiwimatecoder/evals/__init__.py +39 -0
  20. kiwimatecoder/evals/cases.py +230 -0
  21. kiwimatecoder/evals/render.py +46 -0
  22. kiwimatecoder/evals/runner.py +319 -0
  23. kiwimatecoder/events.py +95 -0
  24. kiwimatecoder/forge.py +172 -0
  25. kiwimatecoder/git.py +164 -0
  26. kiwimatecoder/headless.py +255 -0
  27. kiwimatecoder/hooks.py +170 -0
  28. kiwimatecoder/hunks.py +260 -0
  29. kiwimatecoder/i18n.py +368 -0
  30. kiwimatecoder/images.py +195 -0
  31. kiwimatecoder/index/__init__.py +14 -0
  32. kiwimatecoder/index/builder.py +376 -0
  33. kiwimatecoder/index/embeddings.py +151 -0
  34. kiwimatecoder/index/search.py +247 -0
  35. kiwimatecoder/index/store.py +251 -0
  36. kiwimatecoder/instructions.py +83 -0
  37. kiwimatecoder/jobs.py +594 -0
  38. kiwimatecoder/lsp/__init__.py +56 -0
  39. kiwimatecoder/lsp/client.py +548 -0
  40. kiwimatecoder/lsp/manager.py +353 -0
  41. kiwimatecoder/lsp/protocol.py +79 -0
  42. kiwimatecoder/main.py +4258 -0
  43. kiwimatecoder/mcp/__init__.py +45 -0
  44. kiwimatecoder/mcp/client.py +385 -0
  45. kiwimatecoder/mcp/errors.py +7 -0
  46. kiwimatecoder/mcp/manager.py +253 -0
  47. kiwimatecoder/mcp/protocol.py +132 -0
  48. kiwimatecoder/media.py +904 -0
  49. kiwimatecoder/memory.py +141 -0
  50. kiwimatecoder/network.py +112 -0
  51. kiwimatecoder/notify.py +139 -0
  52. kiwimatecoder/permissions.py +171 -0
  53. kiwimatecoder/pickers.py +417 -0
  54. kiwimatecoder/plugins.py +245 -0
  55. kiwimatecoder/pricing.py +167 -0
  56. kiwimatecoder/prompts.py +213 -0
  57. kiwimatecoder/providers.py +342 -0
  58. kiwimatecoder/redaction.py +66 -0
  59. kiwimatecoder/remote.py +294 -0
  60. kiwimatecoder/repl.py +1388 -0
  61. kiwimatecoder/routing.py +65 -0
  62. kiwimatecoder/sandbox.py +169 -0
  63. kiwimatecoder/sdk.py +146 -0
  64. kiwimatecoder/session.py +574 -0
  65. kiwimatecoder/share.py +386 -0
  66. kiwimatecoder/shell.py +660 -0
  67. kiwimatecoder/skills.py +138 -0
  68. kiwimatecoder/sync.py +513 -0
  69. kiwimatecoder/team.py +249 -0
  70. kiwimatecoder/telemetry.py +392 -0
  71. kiwimatecoder/templates.py +108 -0
  72. kiwimatecoder/tools/__init__.py +190 -0
  73. kiwimatecoder/tools/ask.py +62 -0
  74. kiwimatecoder/tools/base.py +69 -0
  75. kiwimatecoder/tools/browser.py +261 -0
  76. kiwimatecoder/tools/edit_file.py +143 -0
  77. kiwimatecoder/tools/forge.py +207 -0
  78. kiwimatecoder/tools/git.py +158 -0
  79. kiwimatecoder/tools/image.py +300 -0
  80. kiwimatecoder/tools/io.py +346 -0
  81. kiwimatecoder/tools/list_dir.py +67 -0
  82. kiwimatecoder/tools/lsp.py +254 -0
  83. kiwimatecoder/tools/memory.py +115 -0
  84. kiwimatecoder/tools/paths.py +234 -0
  85. kiwimatecoder/tools/read_file.py +113 -0
  86. kiwimatecoder/tools/registry.py +91 -0
  87. kiwimatecoder/tools/run_bash.py +189 -0
  88. kiwimatecoder/tools/search.py +224 -0
  89. kiwimatecoder/tools/selection.py +108 -0
  90. kiwimatecoder/tools/shell.py +158 -0
  91. kiwimatecoder/tools/skill.py +46 -0
  92. kiwimatecoder/tools/task.py +61 -0
  93. kiwimatecoder/tools/todo.py +77 -0
  94. kiwimatecoder/tools/web.py +146 -0
  95. kiwimatecoder/tools/write_file.py +83 -0
  96. kiwimatecoder/ui.py +189 -0
  97. kiwimatecoder/updater.py +502 -0
  98. kiwimatecoder/web.py +499 -0
  99. kiwimatecoder-0.1.0.dist-info/METADATA +2016 -0
  100. kiwimatecoder-0.1.0.dist-info/RECORD +104 -0
  101. kiwimatecoder-0.1.0.dist-info/WHEEL +5 -0
  102. kiwimatecoder-0.1.0.dist-info/entry_points.txt +2 -0
  103. kiwimatecoder-0.1.0.dist-info/licenses/LICENSE +21 -0
  104. kiwimatecoder-0.1.0.dist-info/top_level.txt +1 -0
@@ -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}"