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,65 @@
1
+ """Per-turn model routing.
2
+
3
+ A deliberately small, pure heuristic: short, plain user messages can be sent
4
+ to a cheaper "simple" model, while anything that looks like real engineering
5
+ work stays on the session's model. Nothing here touches the network or global
6
+ state, so the decision is trivial to unit-test.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from typing import Any
13
+
14
+ #: A fenced code block means the user is showing/handling code.
15
+ _CODE_FENCE = "```"
16
+
17
+ #: A token like ``main.py`` or ``tests/test_x.py`` looks like a file path.
18
+ _FILE_PATH_RE = re.compile(r"\S+\.\w{1,6}")
19
+
20
+ #: A slash command such as ``/help`` or ``/config set``.
21
+ _SLASH_COMMAND_RE = re.compile(r"(?:^|\s)/[A-Za-z]")
22
+
23
+
24
+ def _contains_exclude_keyword(text: str, keywords: Any) -> bool:
25
+ if not isinstance(keywords, list):
26
+ return False
27
+ lowered = text.lower()
28
+ for keyword in keywords:
29
+ word = str(keyword).strip().lower()
30
+ if word and re.search(rf"\b{re.escape(word)}\b", lowered):
31
+ return True
32
+ return False
33
+
34
+
35
+ def choose_turn_model(
36
+ user_input: str, session: Any, routing: dict[str, Any]
37
+ ) -> str | None:
38
+ """Return the simple model to use for this turn, or None to stay put.
39
+
40
+ The override applies only when routing is enabled, a simple model is
41
+ configured, the message is short, and it shows no sign of real work: no
42
+ code fence, no file path, no slash command, and no exclude keyword
43
+ (whole-word, case-insensitive).
44
+ """
45
+ if not routing.get("enabled"):
46
+ return None
47
+ simple_model = str(routing.get("simple_model") or "").strip()
48
+ if not simple_model:
49
+ return None
50
+ text = user_input or ""
51
+ try:
52
+ max_chars = int(routing.get("simple_max_chars") or 0)
53
+ except (TypeError, ValueError):
54
+ return None
55
+ if max_chars <= 0 or len(text) > max_chars:
56
+ return None
57
+ if _CODE_FENCE in text:
58
+ return None
59
+ if _FILE_PATH_RE.search(text):
60
+ return None
61
+ if _SLASH_COMMAND_RE.search(text):
62
+ return None
63
+ if _contains_exclude_keyword(text, routing.get("exclude_keywords")):
64
+ return None
65
+ return simple_model
@@ -0,0 +1,169 @@
1
+ """Optional OS-level command sandboxing for ``run_bash`` and the shell tools.
2
+
3
+ This module is pure command construction: it decides *what* to execute (macOS
4
+ seatbelt via ``sandbox-exec`` or Linux bubblewrap via ``bwrap``) but never runs
5
+ anything itself. That keeps it trivially testable and lets callers fall back to
6
+ an unsandboxed execution when a backend is missing.
7
+
8
+ The sandbox is **off by default** and is a damage-limiter, not a security
9
+ boundary against a hostile model: the profile still allows broad reads and,
10
+ unless network is disabled, full network access. Only enable it in workspaces
11
+ you trust. Windows has no supported backend here.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import shutil
17
+ import sys
18
+ from collections.abc import Sequence
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from kiwimatecoder.config import get_sandbox
23
+
24
+ FALLBACK_WARNING = (
25
+ "Sandboxing is enabled but no supported backend is available on this "
26
+ "platform; running the command without a sandbox."
27
+ )
28
+
29
+
30
+ def sandbox_available(platform: str | None = None) -> tuple[str, str] | None:
31
+ """Detect the sandbox backend for ``platform`` (defaults to ``sys.platform``).
32
+
33
+ Returns ``(backend, path)`` where ``backend`` is ``"seatbelt"`` on macOS or
34
+ ``"bwrap"`` on Linux, or ``None`` when the platform is unsupported or the
35
+ backend executable is not installed.
36
+ """
37
+ system = platform if platform is not None else sys.platform
38
+ if system == "darwin":
39
+ path = shutil.which("sandbox-exec")
40
+ return ("seatbelt", path) if path else None
41
+ if system.startswith("linux"):
42
+ path = shutil.which("bwrap")
43
+ return ("bwrap", path) if path else None
44
+ return None
45
+
46
+
47
+ def _sbpl_string(value: object) -> str:
48
+ """Quote a path for an SBPL profile string."""
49
+ text = str(value).replace("\\", "\\\\").replace('"', '\\"')
50
+ return f'"{text}"'
51
+
52
+
53
+ def seatbelt_profile(
54
+ *,
55
+ workspace: str | Path,
56
+ extra_writable: Sequence[str] = (),
57
+ network: bool = True,
58
+ ) -> str:
59
+ """Build the seatbelt (SBPL) profile used by :func:`seatbelt_command`.
60
+
61
+ Reads are allowed broadly; writes are limited to the workspace, ``/tmp``
62
+ (and its macOS alias ``/private/tmp``), and any ``extra_writable`` paths.
63
+ Network access is allowed only when ``network`` is true.
64
+ """
65
+ writable = [str(Path(workspace).expanduser()), "/tmp", "/private/tmp"]
66
+ writable.extend(str(Path(path).expanduser()) for path in extra_writable)
67
+ write_rule = " ".join(f"(subpath {_sbpl_string(path)})" for path in writable)
68
+ network_rule = "(allow network*)" if network else "(deny network*)"
69
+ return "\n".join(
70
+ [
71
+ "(version 1)",
72
+ "(deny default)",
73
+ "(allow process*)",
74
+ "(allow file-read*)",
75
+ f"(allow file-write* {write_rule})",
76
+ "(allow sysctl-read)",
77
+ network_rule,
78
+ ]
79
+ )
80
+
81
+
82
+ def seatbelt_command(
83
+ command: str,
84
+ *,
85
+ workspace: str | Path,
86
+ extra_writable: Sequence[str] = (),
87
+ network: bool = True,
88
+ ) -> list[str]:
89
+ """Build ``sandbox-exec -p <profile> /bin/sh -c <command>``."""
90
+ profile = seatbelt_profile(
91
+ workspace=workspace, extra_writable=extra_writable, network=network
92
+ )
93
+ return ["sandbox-exec", "-p", profile, "/bin/sh", "-c", command]
94
+
95
+
96
+ def bwrap_command(
97
+ command: str,
98
+ *,
99
+ workspace: str | Path,
100
+ extra_writable: Sequence[str] = (),
101
+ network: bool = True,
102
+ ) -> list[str]:
103
+ """Build a bubblewrap invocation with a read-only root and writable workspace.
104
+
105
+ ``/`` is bind-mounted read-only, the workspace read-write, ``/tmp`` is a
106
+ private tmpfs, and ``--unshare-net`` is added when ``network`` is false.
107
+ """
108
+ ws = str(Path(workspace).expanduser())
109
+ argv = [
110
+ "bwrap",
111
+ "--ro-bind",
112
+ "/",
113
+ "/",
114
+ "--bind",
115
+ ws,
116
+ ws,
117
+ "--dev",
118
+ "/dev",
119
+ "--proc",
120
+ "/proc",
121
+ "--tmpfs",
122
+ "/tmp",
123
+ ]
124
+ for path in extra_writable:
125
+ resolved = str(Path(path).expanduser())
126
+ argv.extend(["--bind", resolved, resolved])
127
+ if not network:
128
+ argv.append("--unshare-net")
129
+ argv.extend(["/bin/sh", "-c", command])
130
+ return argv
131
+
132
+
133
+ def wrap_command(
134
+ command: str,
135
+ *,
136
+ workspace: str | Path,
137
+ cfg: dict[str, Any] | None = None,
138
+ ) -> tuple[list[str], str | None]:
139
+ """Return the argv to execute plus an optional warning for the caller.
140
+
141
+ Disabled settings return the plain ``["/bin/sh", "-c", command]`` and no
142
+ warning. Enabled settings with no usable backend also return the plain
143
+ argv, with a warning the caller should surface before running.
144
+ """
145
+ settings = get_sandbox(cfg)
146
+ fallback = ["/bin/sh", "-c", command]
147
+ if not settings["enabled"]:
148
+ return fallback, None
149
+ available = sandbox_available()
150
+ if available is None:
151
+ return fallback, FALLBACK_WARNING
152
+ backend, path = available
153
+ if backend == "seatbelt":
154
+ argv = seatbelt_command(
155
+ command,
156
+ workspace=workspace,
157
+ extra_writable=settings["extra_writable"],
158
+ network=settings["network"],
159
+ )
160
+ else:
161
+ argv = bwrap_command(
162
+ command,
163
+ workspace=workspace,
164
+ extra_writable=settings["extra_writable"],
165
+ network=settings["network"],
166
+ )
167
+ if path:
168
+ argv[0] = path
169
+ return argv, None
kiwimatecoder/sdk.py ADDED
@@ -0,0 +1,146 @@
1
+ """Embedded SDK for programmatic, headless agent runs.
2
+
3
+ The SDK wraps the same implementation the CLI's ``-p/--print`` mode uses, so a
4
+ host application gets identical session setup, tool behavior, permissions, and
5
+ cost accounting without spawning a subprocess.
6
+
7
+ Example::
8
+
9
+ from kiwimatecoder.sdk import run_agent_sync
10
+
11
+ result = run_agent_sync("Summarize README.md", workspace=".", mode="plan")
12
+ if result.success:
13
+ print(result.text)
14
+
15
+ ``run_agent`` is the async entry point for hosts that already own an event
16
+ loop. Both entry points return a :class:`RunResult`. Approvals default to
17
+ deny; pass ``confirm`` to grant them (for example
18
+ ``confirm=lambda summary, preview: True``) or run with
19
+ ``mode="auto-accept"``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import Any, Callable
28
+
29
+ from rich.console import Console
30
+
31
+ from kiwimatecoder.headless import run_agent_once
32
+ from kiwimatecoder.permissions import ConfirmFn, PermissionMode
33
+ from kiwimatecoder.session import Session
34
+
35
+ EventCallback = Callable[[str, dict[str, Any]], None]
36
+
37
+
38
+ @dataclass
39
+ class RunResult:
40
+ """The outcome of one programmatic agent run.
41
+
42
+ ``usage`` totals the billed prompt/completion tokens across every model
43
+ call in the turn. ``cost_usd`` is ``None`` when the model has no price
44
+ entry (see :mod:`kiwimatecoder.pricing`). ``error`` is set only when
45
+ ``success`` is False.
46
+ """
47
+
48
+ text: str
49
+ usage: dict[str, int]
50
+ cost_usd: float | None
51
+ provider: str
52
+ model: str
53
+ mode: str
54
+ tools_used: list[str]
55
+ messages: int
56
+ success: bool
57
+ error: str | None = None
58
+
59
+
60
+ async def run_agent(
61
+ prompt: str,
62
+ *,
63
+ workspace: str | Path | None = None,
64
+ provider: str | None = None,
65
+ model: str | None = None,
66
+ mode: PermissionMode | str = PermissionMode.ASK,
67
+ on_event: EventCallback | None = None,
68
+ max_turns: int = 30,
69
+ console: Console | None = None,
70
+ session: Session | None = None,
71
+ confirm: ConfirmFn | None = None,
72
+ ) -> RunResult:
73
+ """Run one agentic turn and return its :class:`RunResult`.
74
+
75
+ ``workspace`` defaults to the current directory. ``provider``, ``model``,
76
+ and ``mode`` override the configured defaults for this run only. Text is
77
+ written to ``console`` when one is supplied, otherwise the run is silent.
78
+ ``on_event`` receives ``(name, payload)`` for every render event. Without
79
+ ``confirm`` (and outside auto-accept/plan modes) approvals are denied.
80
+ """
81
+ outcome = await run_agent_once(
82
+ prompt,
83
+ workspace=workspace,
84
+ provider=provider,
85
+ model=model,
86
+ mode=mode,
87
+ confirm=confirm,
88
+ on_event=on_event,
89
+ max_turns=max_turns,
90
+ console=console,
91
+ session=session,
92
+ render_text=True,
93
+ )
94
+ return RunResult(
95
+ text=outcome.text,
96
+ usage=outcome.usage,
97
+ cost_usd=outcome.cost_usd,
98
+ provider=outcome.provider,
99
+ model=outcome.model,
100
+ mode=outcome.mode,
101
+ tools_used=outcome.tools_used,
102
+ messages=outcome.messages,
103
+ success=outcome.success,
104
+ error=outcome.error,
105
+ )
106
+
107
+
108
+ def run_agent_sync(
109
+ prompt: str,
110
+ *,
111
+ workspace: str | Path | None = None,
112
+ provider: str | None = None,
113
+ model: str | None = None,
114
+ mode: PermissionMode | str = PermissionMode.ASK,
115
+ on_event: EventCallback | None = None,
116
+ max_turns: int = 30,
117
+ console: Console | None = None,
118
+ session: Session | None = None,
119
+ confirm: ConfirmFn | None = None,
120
+ ) -> RunResult:
121
+ """Blocking wrapper around :func:`run_agent`.
122
+
123
+ Raises :class:`RuntimeError` when called from inside a running event loop;
124
+ ``await run_agent(...)`` instead.
125
+ """
126
+ try:
127
+ asyncio.get_running_loop()
128
+ except RuntimeError:
129
+ return asyncio.run(
130
+ run_agent(
131
+ prompt,
132
+ workspace=workspace,
133
+ provider=provider,
134
+ model=model,
135
+ mode=mode,
136
+ on_event=on_event,
137
+ max_turns=max_turns,
138
+ console=console,
139
+ session=session,
140
+ confirm=confirm,
141
+ )
142
+ )
143
+ raise RuntimeError(
144
+ "run_agent_sync() cannot run inside a running event loop; "
145
+ "await run_agent(...) instead."
146
+ )