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
kiwimatecoder/ai.py ADDED
@@ -0,0 +1,68 @@
1
+ """One-shot streaming helper for the ``ask`` command.
2
+
3
+ This is the simple, non-agentic path: send a single prompt and stream the
4
+ answer to the console. It runs on top of :class:`UnifiedClient` so it
5
+ benefits from the full provider registry (OpenRouter unless overridden). There
6
+ is no default model: the caller passes one, or the provider's chosen model is
7
+ used.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from rich.console import Console
15
+ from rich.markup import escape
16
+
17
+ from kiwimatecoder.client import ProviderError, TextDelta, UnifiedClient
18
+ from kiwimatecoder.config import get_prompt_cache, get_sampling, no_model_message, resolve_model
19
+ from kiwimatecoder.providers import ProviderConfig, default_provider
20
+
21
+ console = Console()
22
+
23
+ SYSTEM_PROMPT = (
24
+ "You are KiwiMateCoder, an expert coding assistant. Give clear, concise, and "
25
+ + "accurate coding help. Prefer showing code over lengthy explanations."
26
+ )
27
+
28
+
29
+ async def stream_response(
30
+ prompt: str,
31
+ api_key: str,
32
+ model: str | None = None,
33
+ provider: ProviderConfig | None = None,
34
+ ) -> None:
35
+ """Stream a single answer to the console."""
36
+ provider = provider or default_provider()
37
+ model = model or resolve_model(provider)
38
+ if not model:
39
+ console.print(f"[red]{escape(no_model_message(provider))}[/red]")
40
+ return
41
+ client = UnifiedClient(
42
+ provider,
43
+ api_key,
44
+ sampling=get_sampling(),
45
+ prompt_cache=get_prompt_cache(),
46
+ )
47
+
48
+ messages: list[dict[str, Any]] = [
49
+ {"role": "system", "content": SYSTEM_PROMPT},
50
+ {"role": "user", "content": prompt},
51
+ ]
52
+
53
+ status = console.status("[dim]Thinking…[/dim]")
54
+ status.start()
55
+ thinking = True
56
+ try:
57
+ try:
58
+ async for event in client.stream_chat(messages, tools=None, model=model):
59
+ if isinstance(event, TextDelta):
60
+ if thinking:
61
+ status.stop()
62
+ thinking = False
63
+ console.print(event.text, end="", markup=False, highlight=False)
64
+ finally:
65
+ if thinking:
66
+ status.stop()
67
+ except ProviderError as exc:
68
+ console.print(f"\n[red]{escape(str(exc))}[/red]")
kiwimatecoder/audit.py ADDED
@@ -0,0 +1,65 @@
1
+ """Append-only audit trail of tool actions.
2
+
3
+ Every tool decision (allowed, denied, or dry-run) is written as one JSON line
4
+ to ``~/.kiwimatecoder/audit.log`` with secrets redacted. Logging is best-effort
5
+ so a read-only home directory never breaks a session.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import datetime
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from kiwimatecoder.config import ensure_config_dir
17
+ from kiwimatecoder.redaction import redact
18
+
19
+ AUDIT_LOG_NAME = "audit.log"
20
+
21
+
22
+ def audit_log_path() -> Path:
23
+ """Return the audit log path (resolved lazily so tests can patch CONFIG_DIR)."""
24
+ return ensure_config_dir() / AUDIT_LOG_NAME
25
+
26
+
27
+ def record_tool_event(
28
+ *,
29
+ tool: str,
30
+ args: dict[str, Any],
31
+ decision: str,
32
+ reason: str = "",
33
+ duration_ms: int | None = None,
34
+ ok: bool | None = None,
35
+ hunks: tuple[int, ...] | None = None,
36
+ ) -> None:
37
+ """Append one redacted JSON record; never raises."""
38
+ try:
39
+ encoded_args = redact(json.dumps(args, default=str, ensure_ascii=False))
40
+ except (TypeError, ValueError):
41
+ encoded_args = redact(str(args))
42
+ entry: dict[str, Any] = {
43
+ "timestamp": datetime.datetime.now().isoformat(),
44
+ "tool": tool,
45
+ "args": encoded_args,
46
+ "decision": decision,
47
+ }
48
+ if reason:
49
+ entry["reason"] = redact(reason)
50
+ if duration_ms is not None:
51
+ entry["duration_ms"] = duration_ms
52
+ if ok is not None:
53
+ entry["ok"] = ok
54
+ if hunks is not None:
55
+ entry["hunks"] = list(hunks)
56
+ try:
57
+ path = audit_log_path()
58
+ with path.open("a", encoding="utf-8") as handle:
59
+ handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
60
+ try:
61
+ os.chmod(path, 0o600)
62
+ except OSError:
63
+ pass
64
+ except OSError:
65
+ pass
@@ -0,0 +1,261 @@
1
+ """Optional headless-browser automation backed by Playwright.
2
+
3
+ Playwright is an optional dependency::
4
+
5
+ pip install 'kiwimatecoder[browser]' && playwright install chromium
6
+
7
+ Importing this module never imports ``playwright``; the import happens lazily
8
+ on the first :func:`get_driver` call, so the base install stays dependency-free.
9
+ The sync Playwright API refuses to run inside a running asyncio event loop
10
+ (which is where the agent invokes synchronous tools), so the real driver owns a
11
+ single worker thread and proxies every action onto it. Chromium therefore talks
12
+ to its own thread while the tool call blocks, and successive tool calls share
13
+ one browser page until :func:`reset_driver` closes it.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import datetime
19
+ import threading
20
+ from collections.abc import Callable
21
+ from concurrent.futures import ThreadPoolExecutor
22
+ from pathlib import Path
23
+ from typing import Any, Protocol
24
+
25
+ from kiwimatecoder import config
26
+
27
+ INSTALL_HINT = (
28
+ "Playwright is not installed. Run: "
29
+ "pip install 'kiwimatecoder[browser]' && playwright install chromium"
30
+ )
31
+
32
+ WORKSPACE_STATE_DIR = ".kiwimatecoder"
33
+ SCREENSHOT_DIR_NAME = "screenshots"
34
+
35
+
36
+ class BrowserError(Exception):
37
+ """A user-facing browser failure with a ready-to-show message."""
38
+
39
+
40
+ class Driver(Protocol):
41
+ """Minimal page interface implemented by the Playwright driver and fakes."""
42
+
43
+ def open(self, url: str) -> tuple[str, str]:
44
+ """Navigate to ``url`` and return ``(final_url, title)``."""
45
+ ...
46
+
47
+ def text(self) -> str:
48
+ """Return the visible text of the current page."""
49
+ ...
50
+
51
+ def screenshot(self, path: Path) -> None:
52
+ """Save a PNG screenshot of the current page to ``path``."""
53
+ ...
54
+
55
+ def click(self, selector: str) -> None:
56
+ """Click the first element matching ``selector``."""
57
+ ...
58
+
59
+ def type(self, selector: str, text: str) -> None:
60
+ """Replace the value of the element matching ``selector``."""
61
+ ...
62
+
63
+ def evaluate(self, script: str) -> Any:
64
+ """Run ``script`` in the page and return its (JSON-ish) result."""
65
+ ...
66
+
67
+ def close(self) -> None:
68
+ """Close the page, browser, and Playwright driver."""
69
+ ...
70
+
71
+
72
+ def _friendly(action: str, exc: Exception) -> str:
73
+ """Collapse an exception into one bounded, user-facing line."""
74
+ detail = " ".join(str(exc).split())
75
+ if len(detail) > 500:
76
+ detail = detail[:497] + "..."
77
+ if not detail:
78
+ detail = exc.__class__.__name__
79
+ return f"{action} failed: {detail}"
80
+
81
+
82
+ class _PlaywrightDriver:
83
+ """A Chromium page hosted on a dedicated worker thread."""
84
+
85
+ def __init__(self, settings: dict[str, Any], console: Any = None) -> None:
86
+ self._settings = dict(settings)
87
+ self._console = console
88
+ self._executor = ThreadPoolExecutor(
89
+ max_workers=1, thread_name_prefix="kiwi-browser"
90
+ )
91
+ self._playwright: Any = None
92
+ self._browser: Any = None
93
+ self._page: Any = None
94
+
95
+ # -- thread plumbing ---------------------------------------------------
96
+
97
+ def _run(self, action: str, fn: Callable[..., Any], *args: Any) -> Any:
98
+ future = self._executor.submit(fn, *args)
99
+ try:
100
+ return future.result()
101
+ except BrowserError:
102
+ raise
103
+ except Exception as exc:
104
+ raise BrowserError(_friendly(action, exc)) from exc
105
+
106
+ def start(self) -> None:
107
+ """Import Playwright and launch Chromium, raising ``BrowserError``."""
108
+ self._run("Could not start the browser", self._start)
109
+
110
+ def _start(self) -> None:
111
+ try:
112
+ from playwright.sync_api import sync_playwright # type: ignore[import-not-found]
113
+ except ImportError as exc:
114
+ raise BrowserError(INSTALL_HINT) from exc
115
+ try:
116
+ self._playwright = sync_playwright().start()
117
+ self._browser = self._playwright.chromium.launch(
118
+ headless=bool(self._settings.get("headless", True))
119
+ )
120
+ self._page = self._browser.new_page()
121
+ timeout = int(self._settings.get("timeout_ms", 15000))
122
+ self._page.set_default_timeout(timeout)
123
+ self._page.set_default_navigation_timeout(timeout)
124
+ except Exception as exc:
125
+ self._close_impl()
126
+ raise BrowserError(_friendly("Could not start Chromium", exc)) from exc
127
+
128
+ def _ensure_page(self) -> None:
129
+ if self._page is None:
130
+ raise BrowserError("No page is open. Use action='open' with a URL first.")
131
+
132
+ def _close_impl(self) -> None:
133
+ page, self._page = self._page, None
134
+ browser, self._browser = self._browser, None
135
+ playwright, self._playwright = self._playwright, None
136
+ for obj in (page, browser):
137
+ if obj is not None:
138
+ try:
139
+ obj.close()
140
+ except Exception:
141
+ pass
142
+ if playwright is not None:
143
+ try:
144
+ playwright.stop()
145
+ except Exception:
146
+ pass
147
+
148
+ def close(self) -> None:
149
+ try:
150
+ self._run("Could not close the browser", self._close_impl)
151
+ finally:
152
+ self._executor.shutdown(wait=False)
153
+
154
+ # -- Driver actions ----------------------------------------------------
155
+
156
+ def open(self, url: str) -> tuple[str, str]:
157
+ def action() -> tuple[str, str]:
158
+ self._ensure_page()
159
+ response = self._page.goto(url, wait_until="domcontentloaded")
160
+ final_url = str(
161
+ getattr(response, "url", None) or getattr(self._page, "url", "") or url
162
+ )
163
+ try:
164
+ title = str(self._page.title() or "")
165
+ except Exception:
166
+ title = ""
167
+ return final_url, title
168
+
169
+ return self._run("Opening the page", action)
170
+
171
+ def text(self) -> str:
172
+ def action() -> str:
173
+ self._ensure_page()
174
+ return str(self._page.inner_text("body") or "")
175
+
176
+ return self._run("Reading the page", action)
177
+
178
+ def screenshot(self, path: Path) -> None:
179
+ def action() -> None:
180
+ self._ensure_page()
181
+ self._page.screenshot(path=str(path), full_page=True)
182
+
183
+ self._run("Taking the screenshot", action)
184
+
185
+ def click(self, selector: str) -> None:
186
+ def action() -> None:
187
+ self._ensure_page()
188
+ self._page.click(selector)
189
+
190
+ self._run("Clicking the element", action)
191
+
192
+ def type(self, selector: str, text: str) -> None:
193
+ def action() -> None:
194
+ self._ensure_page()
195
+ self._page.fill(selector, text)
196
+
197
+ self._run("Typing into the element", action)
198
+
199
+ def evaluate(self, script: str) -> Any:
200
+ def action() -> Any:
201
+ self._ensure_page()
202
+ return self._page.evaluate(script)
203
+
204
+ return self._run("Evaluating the script", action)
205
+
206
+ def set_timeout(self, timeout_ms: int) -> None:
207
+ def action() -> None:
208
+ self._ensure_page()
209
+ self._page.set_default_timeout(timeout_ms)
210
+
211
+ self._run("Setting the timeout", action)
212
+
213
+
214
+ _DRIVER: Driver | None = None
215
+ _LOCK = threading.Lock()
216
+
217
+
218
+ def get_driver(
219
+ settings: dict[str, Any] | None = None, console: Any = None
220
+ ) -> Driver:
221
+ """Return the singleton driver, launching Chromium on first use."""
222
+ global _DRIVER
223
+ with _LOCK:
224
+ if _DRIVER is not None:
225
+ return _DRIVER
226
+ effective = settings if settings is not None else config.get_browser()
227
+ driver = _PlaywrightDriver(effective, console)
228
+ try:
229
+ driver.start()
230
+ except Exception:
231
+ driver.close()
232
+ raise
233
+ _DRIVER = driver
234
+ return _DRIVER
235
+
236
+
237
+ def current_driver() -> Driver | None:
238
+ """Return the live driver without creating one (None before ``open``)."""
239
+ return _DRIVER
240
+
241
+
242
+ def reset_driver() -> None:
243
+ """Close the singleton driver, if any. Never raises."""
244
+ global _DRIVER
245
+ with _LOCK:
246
+ driver, _DRIVER = _DRIVER, None
247
+ if driver is not None:
248
+ try:
249
+ driver.close()
250
+ except Exception:
251
+ pass
252
+
253
+
254
+ def screenshot_path(workspace_root: str | Path) -> Path:
255
+ """Return a fresh screenshot path under the workspace, creating dirs."""
256
+ directory = (
257
+ Path(workspace_root) / WORKSPACE_STATE_DIR / SCREENSHOT_DIR_NAME
258
+ )
259
+ directory.mkdir(parents=True, exist_ok=True)
260
+ stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
261
+ return directory / f"{stamp}.png"