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,353 @@
1
+ """Language-server lifecycle: detect, route by extension, start lazily.
2
+
3
+ Built-in presets cover the common languages and are used when their command is
4
+ on ``PATH``; entries under ``lsp.servers`` override or extend them. Servers are
5
+ started on first use for the language that owns a file's extension, and a failed
6
+ start is recorded so later calls fail fast with a clear reason instead of
7
+ re-spawning. ``shutdown`` stops every live client and clears recorded failures.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shutil
13
+ import time
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from rich.console import Console
19
+
20
+ from kiwimatecoder import config
21
+ from kiwimatecoder.lsp.client import LspClient, LspError, LspTimeout
22
+
23
+ INITIALIZE_TIMEOUT = 10.0
24
+
25
+ # name -> {command, args, extensions}. The manager also merges user overrides
26
+ # from the ``lsp.servers`` config section on top of these.
27
+ SERVER_PRESETS: dict[str, dict[str, Any]] = {
28
+ "python": {
29
+ "command": "pyright-langserver",
30
+ "args": ["--stdio"],
31
+ "extensions": [".py", ".pyi"],
32
+ },
33
+ "typescript": {
34
+ "command": "typescript-language-server",
35
+ "args": ["--stdio"],
36
+ "extensions": [".ts", ".tsx", ".js", ".jsx"],
37
+ },
38
+ "go": {"command": "gopls", "args": [], "extensions": [".go"]},
39
+ "rust": {"command": "rust-analyzer", "args": [], "extensions": [".rs"]},
40
+ "c/cpp": {
41
+ "command": "clangd",
42
+ "args": [],
43
+ "extensions": [".c", ".h", ".cpp", ".hpp"],
44
+ },
45
+ }
46
+
47
+ LSP_DISABLED_MESSAGE = (
48
+ "LSP is disabled. Enable it with /lsp on or "
49
+ "`kiwimatecoder config lsp enable on`."
50
+ )
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class LspServerSpec:
55
+ """The effective command and file extensions for one language server."""
56
+
57
+ name: str
58
+ command: str
59
+ args: tuple[str, ...]
60
+ extensions: tuple[str, ...]
61
+
62
+
63
+ def _spec(name: str, preset: dict[str, Any]) -> LspServerSpec:
64
+ return LspServerSpec(
65
+ name=name,
66
+ command=str(preset["command"]),
67
+ args=tuple(str(arg) for arg in preset.get("args") or ()),
68
+ extensions=tuple(str(ext) for ext in preset.get("extensions") or ()),
69
+ )
70
+
71
+
72
+ def effective_servers() -> dict[str, LspServerSpec]:
73
+ """Return presets merged with ``lsp.servers`` overrides (no PATH check).
74
+
75
+ User-configured entries are listed first, so an override or a custom
76
+ server wins extension routing over a built-in preset with the same suffix.
77
+ """
78
+ specs: dict[str, LspServerSpec] = {}
79
+ for name, override in config.get_lsp()["servers"].items():
80
+ preset = SERVER_PRESETS.get(name)
81
+ if preset is None:
82
+ specs[name] = LspServerSpec(
83
+ name=name,
84
+ command=str(override["command"]),
85
+ args=tuple(str(arg) for arg in override["args"]),
86
+ extensions=tuple(str(ext) for ext in override["extensions"]),
87
+ )
88
+ else:
89
+ specs[name] = LspServerSpec(
90
+ name=name,
91
+ command=str(override["command"] or preset["command"]),
92
+ args=tuple(
93
+ str(arg) for arg in (override["args"] or preset["args"])
94
+ ),
95
+ extensions=tuple(
96
+ str(ext)
97
+ for ext in (override["extensions"] or preset["extensions"])
98
+ ),
99
+ )
100
+ for name, preset in SERVER_PRESETS.items():
101
+ if name not in specs:
102
+ specs[name] = _spec(name, preset)
103
+ return specs
104
+
105
+
106
+ def available_servers() -> dict[str, LspServerSpec]:
107
+ """Effective servers whose command resolves on ``PATH``."""
108
+ return {
109
+ name: spec
110
+ for name, spec in effective_servers().items()
111
+ if shutil.which(spec.command)
112
+ }
113
+
114
+
115
+ class LspManager:
116
+ """Owns live LSP clients and routes files to them by extension."""
117
+
118
+ def __init__(
119
+ self,
120
+ console: Console | None = None,
121
+ root: str | Path | None = None,
122
+ ) -> None:
123
+ self._console = console if console is not None else Console()
124
+ self._root = (
125
+ Path(root).expanduser().resolve() if root is not None else Path.cwd()
126
+ )
127
+ self._clients: dict[str, LspClient] = {}
128
+ self._failures: dict[str, str] = {}
129
+
130
+ @property
131
+ def root(self) -> Path:
132
+ """The workspace root used for relative paths and server handshakes."""
133
+ return self._root
134
+
135
+ def set_root(self, root: str | Path) -> None:
136
+ """Point the manager at a new workspace root."""
137
+ self._root = Path(root).expanduser().resolve()
138
+
139
+ @property
140
+ def clients(self) -> dict[str, LspClient]:
141
+ """A copy of the language -> live client map."""
142
+ return dict(self._clients)
143
+
144
+ def failure_for(self, language: str) -> str | None:
145
+ """The recorded start failure for ``language``, if any."""
146
+ return self._failures.get(language)
147
+
148
+ def servers(self) -> dict[str, LspServerSpec]:
149
+ """Effective servers: presets merged with ``lsp.servers`` overrides."""
150
+ return effective_servers()
151
+
152
+ def available_servers(self) -> dict[str, LspServerSpec]:
153
+ """Effective servers whose command resolves on ``PATH``."""
154
+ return {
155
+ name: spec
156
+ for name, spec in effective_servers().items()
157
+ if shutil.which(spec.command)
158
+ }
159
+
160
+ def language_for(self, path: str | Path) -> str | None:
161
+ """Return the language whose extensions claim ``path``, if any."""
162
+ suffix = Path(path).suffix.lower()
163
+ if not suffix:
164
+ return None
165
+ for name, spec in self.servers().items():
166
+ if suffix in spec.extensions:
167
+ return name
168
+ return None
169
+
170
+ def diagnostics_for(
171
+ self, path: str | Path, timeout: float | None = None
172
+ ) -> list[dict[str, Any]]:
173
+ """Return diagnostics for ``path``, starting its server on first use."""
174
+ settings = config.get_lsp()
175
+ if not settings["enabled"]:
176
+ raise LspError(LSP_DISABLED_MESSAGE)
177
+ deadline = self._deadline(settings["timeout"], timeout)
178
+ client, resolved = self._prepare(path, deadline)
179
+ client.sync(resolved, _read_text(resolved))
180
+ remaining = max(0.05, deadline - time.monotonic())
181
+ return client.diagnostics(resolved, remaining)
182
+
183
+ def definition(
184
+ self,
185
+ path: str | Path,
186
+ line: int,
187
+ character: int,
188
+ timeout: float | None = None,
189
+ ) -> list[dict[str, Any]]:
190
+ """Return definition locations for a 1-based position."""
191
+ return self._locations(
192
+ "definition", path, line, character, None, timeout
193
+ )
194
+
195
+ def references(
196
+ self,
197
+ path: str | Path,
198
+ line: int,
199
+ character: int,
200
+ include_declaration: bool = True,
201
+ timeout: float | None = None,
202
+ ) -> list[dict[str, Any]]:
203
+ """Return reference locations for a 1-based position."""
204
+ return self._locations(
205
+ "references", path, line, character, include_declaration, timeout
206
+ )
207
+
208
+ def shutdown(self) -> None:
209
+ """Stop every live client and clear recorded failures."""
210
+ for language in list(self._clients):
211
+ client = self._clients.pop(language)
212
+ try:
213
+ client.shutdown()
214
+ except Exception:
215
+ pass
216
+ self._failures.clear()
217
+
218
+ # -- internals ----------------------------------------------------------
219
+
220
+ @staticmethod
221
+ def _deadline(configured: float, timeout: float | None) -> float:
222
+ budget = float(configured) if timeout is None else min(
223
+ float(timeout), float(configured)
224
+ )
225
+ return time.monotonic() + max(0.05, budget)
226
+
227
+ def _locations(
228
+ self,
229
+ action: str,
230
+ path: str | Path,
231
+ line: int,
232
+ character: int,
233
+ include_declaration: bool | None,
234
+ timeout: float | None,
235
+ ) -> list[dict[str, Any]]:
236
+ settings = config.get_lsp()
237
+ if not settings["enabled"]:
238
+ raise LspError(LSP_DISABLED_MESSAGE)
239
+ deadline = self._deadline(settings["timeout"], timeout)
240
+ client, resolved = self._prepare(path, deadline)
241
+ client.sync(resolved, _read_text(resolved))
242
+ remaining = max(0.05, deadline - time.monotonic())
243
+ if action == "definition":
244
+ return client.definition(
245
+ resolved, int(line) - 1, int(character) - 1, timeout=remaining
246
+ )
247
+ return client.references(
248
+ resolved,
249
+ int(line) - 1,
250
+ int(character) - 1,
251
+ include_declaration=bool(include_declaration),
252
+ timeout=remaining,
253
+ )
254
+
255
+ def _prepare(
256
+ self, path: str | Path, deadline: float
257
+ ) -> tuple[LspClient, Path]:
258
+ resolved = Path(path).expanduser()
259
+ if not resolved.is_absolute():
260
+ resolved = self._root / resolved
261
+ resolved = resolved.resolve()
262
+ if not resolved.is_file():
263
+ raise LspError(f"File not found: {path}")
264
+ language = self.language_for(resolved)
265
+ if language is None:
266
+ raise LspError(
267
+ f"No language server is configured for {resolved.name}."
268
+ )
269
+ return self._client_for(language, deadline), resolved
270
+
271
+ def _client_for(
272
+ self, language: str, deadline: float | None = None
273
+ ) -> LspClient:
274
+ client = self._clients.get(language)
275
+ if client is not None:
276
+ if client.running:
277
+ return client
278
+ client.close()
279
+ self._clients.pop(language, None)
280
+ failure = self._failures.get(language)
281
+ if failure:
282
+ raise LspError(failure)
283
+ spec = self.servers().get(language)
284
+ if spec is None:
285
+ raise LspError(f"Unknown language server '{language}'.")
286
+ command = shutil.which(spec.command)
287
+ if command is None:
288
+ reason = (
289
+ f"language server command '{spec.command}' was not found on PATH."
290
+ )
291
+ self._failures[language] = reason
292
+ raise LspError(reason)
293
+
294
+ initialize_timeout = INITIALIZE_TIMEOUT
295
+ if deadline is not None:
296
+ remaining = deadline - time.monotonic()
297
+ if remaining <= 0:
298
+ raise LspTimeout(
299
+ f"no time left to start the '{language}' language server"
300
+ )
301
+ initialize_timeout = min(initialize_timeout, remaining)
302
+ started: LspClient | None = None
303
+ try:
304
+ started = LspClient(
305
+ command,
306
+ spec.args,
307
+ root=self._root,
308
+ initialize_timeout=initialize_timeout,
309
+ request_timeout=initialize_timeout,
310
+ )
311
+ started.initialize()
312
+ except Exception as exc:
313
+ if started is not None:
314
+ started.close()
315
+ reason = f"could not start '{spec.command}': {exc}"
316
+ self._failures[language] = reason
317
+ raise LspError(reason) from exc
318
+ self._clients[language] = started
319
+ return started
320
+
321
+
322
+ def _read_text(path: Path) -> str:
323
+ try:
324
+ return path.read_text(encoding="utf-8", errors="replace")
325
+ except OSError as exc:
326
+ raise LspError(f"Could not read {path}: {exc}") from exc
327
+
328
+
329
+ _MANAGER: LspManager | None = None
330
+
331
+
332
+ def get_manager() -> LspManager | None:
333
+ """Return the session's LSP manager, if one has been created."""
334
+ return _MANAGER
335
+
336
+
337
+ def set_manager(manager: LspManager | None) -> None:
338
+ """Install (or clear) the session's LSP manager."""
339
+ global _MANAGER
340
+ _MANAGER = manager
341
+
342
+
343
+ def ensure_manager(
344
+ console: Console | None = None, root: str | Path | None = None
345
+ ) -> LspManager:
346
+ """Return the session manager, creating it on first use."""
347
+ manager = get_manager()
348
+ if manager is None:
349
+ manager = LspManager(console=console, root=root)
350
+ set_manager(manager)
351
+ elif root is not None:
352
+ manager.set_root(root)
353
+ return manager
@@ -0,0 +1,79 @@
1
+ """LSP base-protocol framing: ``Content-Length`` headers around JSON payloads.
2
+
3
+ The codec is pure (no I/O) so it can be unit-tested directly. ``encode_message``
4
+ produces one complete frame; ``decode_messages`` consumes a possibly-partial
5
+ byte buffer and returns every complete message plus the unparsed remainder, so
6
+ a reader can call it again as more bytes arrive.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Any
13
+
14
+ HEADER_SEPARATOR = b"\r\n\r\n"
15
+ ALT_HEADER_SEPARATOR = b"\n\n"
16
+
17
+
18
+ class LspProtocolError(ValueError):
19
+ """Raised when a frame header or body cannot be decoded."""
20
+
21
+
22
+ def encode_message(message: dict[str, Any]) -> bytes:
23
+ """Encode ``message`` as one LSP frame (header + UTF-8 JSON body)."""
24
+ body = json.dumps(
25
+ message, separators=(",", ":"), ensure_ascii=False
26
+ ).encode("utf-8")
27
+ header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
28
+ return header + body
29
+
30
+
31
+ def decode_messages(buffer: bytes) -> tuple[list[dict[str, Any]], bytes]:
32
+ """Decode every complete frame in ``buffer``; return (messages, remainder).
33
+
34
+ A partial header or body is left in the remainder for the next call. A
35
+ malformed header or body raises :class:`LspProtocolError`; callers decide
36
+ whether to discard the connection.
37
+ """
38
+ messages: list[dict[str, Any]] = []
39
+ while buffer:
40
+ split = buffer.find(HEADER_SEPARATOR)
41
+ separator_length = len(HEADER_SEPARATOR)
42
+ if split < 0:
43
+ split = buffer.find(ALT_HEADER_SEPARATOR)
44
+ separator_length = len(ALT_HEADER_SEPARATOR)
45
+ if split < 0:
46
+ return messages, buffer # incomplete header
47
+ header = buffer[:split].decode("ascii", "replace")
48
+ length = _content_length(header)
49
+ body_start = split + separator_length
50
+ if len(buffer) - body_start < length:
51
+ return messages, buffer # incomplete body
52
+ body = buffer[body_start : body_start + length]
53
+ try:
54
+ message = json.loads(body.decode("utf-8"))
55
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
56
+ raise LspProtocolError(f"invalid JSON body: {exc}") from exc
57
+ if not isinstance(message, dict):
58
+ raise LspProtocolError("message body must be a JSON object")
59
+ messages.append(message)
60
+ buffer = buffer[body_start + length :]
61
+ return messages, buffer
62
+
63
+
64
+ def _content_length(header: str) -> int:
65
+ """Return the ``Content-Length`` value from a frame header."""
66
+ for line in header.replace("\r\n", "\n").split("\n"):
67
+ name, _, value = line.partition(":")
68
+ if name.strip().lower() != "content-length":
69
+ continue
70
+ try:
71
+ length = int(value.strip())
72
+ except ValueError as exc:
73
+ raise LspProtocolError(
74
+ f"invalid Content-Length: {value.strip()!r}"
75
+ ) from exc
76
+ if length < 0:
77
+ raise LspProtocolError("negative Content-Length")
78
+ return length
79
+ raise LspProtocolError("missing Content-Length header")