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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""edit_file tool: replace a unique string in an existing file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from kiwimatecoder.session import Session
|
|
9
|
+
from kiwimatecoder.tools.base import FunctionTool, ToolResult
|
|
10
|
+
from kiwimatecoder.tools.paths import (
|
|
11
|
+
PathError,
|
|
12
|
+
atomic_write_text,
|
|
13
|
+
display_path,
|
|
14
|
+
resolve_in_workspace,
|
|
15
|
+
workspace_ref,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EditError(ValueError):
|
|
20
|
+
"""Raised when an edit cannot be applied unambiguously."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compute_edit(text: str, old: str, new: str, replace_all: bool) -> str:
|
|
24
|
+
"""Apply the edit to ``text`` or raise :class:`EditError`."""
|
|
25
|
+
if old == new:
|
|
26
|
+
raise EditError("old_string and new_string are identical")
|
|
27
|
+
count = text.count(old)
|
|
28
|
+
if count == 0:
|
|
29
|
+
raise EditError("old_string not found in file")
|
|
30
|
+
if count > 1 and not replace_all:
|
|
31
|
+
raise EditError(
|
|
32
|
+
f"old_string is not unique ({count} matches); add surrounding context to disambiguate or set replace_all=true"
|
|
33
|
+
)
|
|
34
|
+
if replace_all:
|
|
35
|
+
return text.replace(old, new)
|
|
36
|
+
return text.replace(old, new, 1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _edit_from_args(text: str, args: dict[str, Any]) -> str:
|
|
40
|
+
"""Compute the edit described by ``args``; raises :class:`EditError`."""
|
|
41
|
+
return compute_edit(
|
|
42
|
+
text,
|
|
43
|
+
str(args.get("old_string") or ""),
|
|
44
|
+
str(args.get("new_string") or ""),
|
|
45
|
+
bool(args.get("replace_all")),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_new_content(old_text: str, args: dict[str, Any]) -> str | None:
|
|
50
|
+
"""Return the content after applying ``args``, or None when it can't apply."""
|
|
51
|
+
try:
|
|
52
|
+
return _edit_from_args(old_text, args)
|
|
53
|
+
except EditError:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _diff(old_text: str, new_text: str, rel: str) -> str:
|
|
58
|
+
return "".join(
|
|
59
|
+
difflib.unified_diff(
|
|
60
|
+
old_text.splitlines(keepends=True),
|
|
61
|
+
new_text.splitlines(keepends=True),
|
|
62
|
+
fromfile=rel,
|
|
63
|
+
tofile=rel,
|
|
64
|
+
n=3,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def preview(args: dict[str, Any], session: Session) -> str:
|
|
70
|
+
"""Return a unified diff for the approval prompt."""
|
|
71
|
+
path = str(args.get("path") or "")
|
|
72
|
+
try:
|
|
73
|
+
resolved = resolve_in_workspace(path, session.workspace_root)
|
|
74
|
+
except PathError as exc:
|
|
75
|
+
return str(exc)
|
|
76
|
+
if not resolved.exists():
|
|
77
|
+
return f"(file not found: {path})"
|
|
78
|
+
rel = display_path(resolved, session.workspace_root)
|
|
79
|
+
old_text = resolved.read_text(encoding="utf-8", errors="replace")
|
|
80
|
+
try:
|
|
81
|
+
new_text = _edit_from_args(old_text, args)
|
|
82
|
+
except EditError as exc:
|
|
83
|
+
return f"(cannot apply edit: {exc})"
|
|
84
|
+
return _diff(old_text, new_text, rel) or "(no changes)"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _edit_file(args: dict[str, Any], session: Session) -> ToolResult:
|
|
88
|
+
path = str(args.get("path") or "")
|
|
89
|
+
if not path:
|
|
90
|
+
return ToolResult.error("'path' is required")
|
|
91
|
+
try:
|
|
92
|
+
resolved = resolve_in_workspace(path, session.workspace_root)
|
|
93
|
+
except PathError as exc:
|
|
94
|
+
return ToolResult.error(str(exc))
|
|
95
|
+
if not resolved.exists():
|
|
96
|
+
return ToolResult.error(f"File not found: {path}")
|
|
97
|
+
|
|
98
|
+
old_text = resolved.read_text(encoding="utf-8", errors="replace")
|
|
99
|
+
try:
|
|
100
|
+
new_text = _edit_from_args(old_text, args)
|
|
101
|
+
except EditError as exc:
|
|
102
|
+
return ToolResult.error(str(exc))
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
atomic_write_text(resolved, new_text)
|
|
106
|
+
except OSError as exc:
|
|
107
|
+
return ToolResult.error(f"Could not write file: {exc}")
|
|
108
|
+
session.record_touched(workspace_ref(resolved, session.workspace_root))
|
|
109
|
+
return ToolResult(content=f"Edited {path}.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
edit_file_tool = FunctionTool(
|
|
113
|
+
name="edit_file",
|
|
114
|
+
description=(
|
|
115
|
+
"Replace an exact string in an existing file. old_string must match "
|
|
116
|
+
"uniquely unless replace_all is true. Include enough surrounding context "
|
|
117
|
+
"to make the match unambiguous."
|
|
118
|
+
),
|
|
119
|
+
parameters={
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {
|
|
122
|
+
"path": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Path relative to the workspace root.",
|
|
125
|
+
},
|
|
126
|
+
"old_string": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Exact text to find and replace.",
|
|
129
|
+
},
|
|
130
|
+
"new_string": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"description": "Replacement text.",
|
|
133
|
+
},
|
|
134
|
+
"replace_all": {
|
|
135
|
+
"type": "boolean",
|
|
136
|
+
"description": "Replace every occurrence instead of requiring uniqueness.",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
"required": ["path", "old_string", "new_string"],
|
|
140
|
+
},
|
|
141
|
+
func=_edit_file,
|
|
142
|
+
writes=True,
|
|
143
|
+
)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""forge (read-only) and forge_write (approval-gated) tools.
|
|
2
|
+
|
|
3
|
+
Both delegate to the official forge CLI (``gh``/``glab``) so the user's
|
|
4
|
+
existing login is used. Only list/view and create actions are exposed; merge,
|
|
5
|
+
close, and delete are intentionally not implemented.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shlex
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from kiwimatecoder import forge as forge_module
|
|
14
|
+
from kiwimatecoder.session import Session
|
|
15
|
+
from kiwimatecoder.tools.base import FunctionTool, ToolResult
|
|
16
|
+
|
|
17
|
+
READ_ACTIONS = forge_module.READ_ACTIONS
|
|
18
|
+
WRITE_ACTIONS = forge_module.WRITE_ACTIONS
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _truncate(text: str) -> str:
|
|
22
|
+
if len(text) > forge_module.MAX_OUTPUT:
|
|
23
|
+
return text[: forge_module.MAX_OUTPUT] + "\n... [output truncated]"
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _render(cli: str, action: str, code: int, output: str) -> ToolResult:
|
|
28
|
+
output = output.strip()
|
|
29
|
+
body = output if output else "(no output)"
|
|
30
|
+
content = f"{cli} {action}:\n{_truncate(body)}\n[exit code: {code}]"
|
|
31
|
+
return ToolResult(content=content, ok=code == 0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve(session: Session) -> tuple[str | None, str | None, ToolResult | None]:
|
|
35
|
+
"""Detect the forge and CLI, or return a clear error result."""
|
|
36
|
+
remote = forge_module.current_remote_url(session)
|
|
37
|
+
if not remote:
|
|
38
|
+
return (
|
|
39
|
+
None,
|
|
40
|
+
None,
|
|
41
|
+
ToolResult.error(
|
|
42
|
+
"No git remote 'origin' found in this workspace. Add one with "
|
|
43
|
+
"`git remote add origin <url>` and try again."
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
kind = forge_module.detect_forge(remote)
|
|
47
|
+
if kind is None:
|
|
48
|
+
return (
|
|
49
|
+
None,
|
|
50
|
+
None,
|
|
51
|
+
ToolResult.error(
|
|
52
|
+
f"Remote '{remote}' is not a supported forge host. Only "
|
|
53
|
+
"github.com and gitlab.com remotes are supported."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
cli = forge_module.forge_cli(kind)
|
|
57
|
+
if not cli or not forge_module.cli_available(cli):
|
|
58
|
+
return (
|
|
59
|
+
None,
|
|
60
|
+
None,
|
|
61
|
+
ToolResult.error(
|
|
62
|
+
f"The '{cli or kind}' CLI is required for {kind} but was not "
|
|
63
|
+
"found on PATH. Install and authenticate it, then try again."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
return kind, cli, None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _number(args: dict[str, Any]) -> tuple[int | None, ToolResult | None]:
|
|
70
|
+
raw = args.get("number")
|
|
71
|
+
if raw is None:
|
|
72
|
+
return None, ToolResult.error("'number' must be an integer")
|
|
73
|
+
try:
|
|
74
|
+
number = int(raw)
|
|
75
|
+
except (TypeError, ValueError):
|
|
76
|
+
return None, ToolResult.error("'number' must be an integer")
|
|
77
|
+
if number < 1:
|
|
78
|
+
return None, ToolResult.error("'number' must be a positive integer")
|
|
79
|
+
return number, None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _forge(args: dict[str, Any], session: Session) -> ToolResult:
|
|
83
|
+
kind, cli, error = _resolve(session)
|
|
84
|
+
if error is not None:
|
|
85
|
+
return error
|
|
86
|
+
assert kind is not None and cli is not None
|
|
87
|
+
|
|
88
|
+
action = str(args.get("action") or "").strip().lower()
|
|
89
|
+
if action not in READ_ACTIONS:
|
|
90
|
+
return ToolResult.error(
|
|
91
|
+
f"Unknown forge action '{action}'. Choose: {', '.join(READ_ACTIONS)}."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
command: list[str]
|
|
95
|
+
if action == "pr_list":
|
|
96
|
+
command = forge_module.pr_list_args(kind)
|
|
97
|
+
elif action == "issue_list":
|
|
98
|
+
command = forge_module.issue_list_args()
|
|
99
|
+
elif action == "pr_view":
|
|
100
|
+
number, number_error = _number(args)
|
|
101
|
+
if number_error is not None:
|
|
102
|
+
return number_error
|
|
103
|
+
command = forge_module.pr_view_args(number if number is not None else 0, forge=kind)
|
|
104
|
+
else: # issue_view
|
|
105
|
+
number, number_error = _number(args)
|
|
106
|
+
if number_error is not None:
|
|
107
|
+
return number_error
|
|
108
|
+
command = forge_module.issue_view_args(number if number is not None else 0)
|
|
109
|
+
|
|
110
|
+
code, output = forge_module.run_forge(cli, command, session)
|
|
111
|
+
return _render(cli, action, code, output)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _forge_write(args: dict[str, Any], session: Session) -> ToolResult:
|
|
115
|
+
kind, cli, error = _resolve(session)
|
|
116
|
+
if error is not None:
|
|
117
|
+
return error
|
|
118
|
+
assert kind is not None and cli is not None
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
command = forge_module.build_write_args(kind, args)
|
|
122
|
+
except ValueError as exc:
|
|
123
|
+
return ToolResult.error(str(exc))
|
|
124
|
+
|
|
125
|
+
code, output = forge_module.run_forge(cli, command, session)
|
|
126
|
+
action = str(args.get("action") or "").strip().lower()
|
|
127
|
+
return _render(cli, action, code, output)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def preview(args: dict[str, Any], session: Session) -> str:
|
|
131
|
+
"""Return the exact CLI command line for the approval prompt."""
|
|
132
|
+
kind, cli, error = _resolve(session)
|
|
133
|
+
if error is not None:
|
|
134
|
+
return error.content
|
|
135
|
+
assert kind is not None and cli is not None
|
|
136
|
+
try:
|
|
137
|
+
command = forge_module.build_write_args(kind, args)
|
|
138
|
+
except ValueError as exc:
|
|
139
|
+
return str(exc)
|
|
140
|
+
return f"{cli} " + " ".join(shlex.quote(part) for part in command)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
forge_tool = FunctionTool(
|
|
144
|
+
name="forge",
|
|
145
|
+
description=(
|
|
146
|
+
"Read GitHub/GitLab data through the gh/glab CLI: list or view pull "
|
|
147
|
+
"requests (merge requests on GitLab) and issues. The forge is detected "
|
|
148
|
+
"from the origin remote. Requires the matching CLI to be installed and "
|
|
149
|
+
"authenticated."
|
|
150
|
+
),
|
|
151
|
+
parameters={
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"action": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"enum": list(READ_ACTIONS),
|
|
157
|
+
"description": "Which forge query to run.",
|
|
158
|
+
},
|
|
159
|
+
"number": {
|
|
160
|
+
"type": "integer",
|
|
161
|
+
"description": "PR/MR or issue number for the view actions.",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
"required": ["action"],
|
|
165
|
+
},
|
|
166
|
+
func=_forge,
|
|
167
|
+
writes=False,
|
|
168
|
+
runs=False,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
forge_write_tool = FunctionTool(
|
|
172
|
+
name="forge_write",
|
|
173
|
+
description=(
|
|
174
|
+
"Create a pull request (merge request on GitLab) or an issue using "
|
|
175
|
+
"gh/glab. Requires user approval. Merge, close, and delete are "
|
|
176
|
+
"intentionally unavailable."
|
|
177
|
+
),
|
|
178
|
+
parameters={
|
|
179
|
+
"type": "object",
|
|
180
|
+
"properties": {
|
|
181
|
+
"action": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"enum": list(WRITE_ACTIONS),
|
|
184
|
+
"description": "Which forge object to create.",
|
|
185
|
+
},
|
|
186
|
+
"title": {
|
|
187
|
+
"type": "string",
|
|
188
|
+
"description": "Title for the PR/MR or issue.",
|
|
189
|
+
},
|
|
190
|
+
"body": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": "Body text for the PR/MR or issue.",
|
|
193
|
+
},
|
|
194
|
+
"base": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"description": "Base/target branch for a PR/MR.",
|
|
197
|
+
},
|
|
198
|
+
"draft": {
|
|
199
|
+
"type": "boolean",
|
|
200
|
+
"description": "Open a PR/MR as a draft.",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"required": ["action", "title"],
|
|
204
|
+
},
|
|
205
|
+
func=_forge_write,
|
|
206
|
+
writes=True,
|
|
207
|
+
)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""git (read-only) and git_write (approval-gated) tools.
|
|
2
|
+
|
|
3
|
+
The read-only tool is safe to advertise in plan mode. ``git_write`` is marked
|
|
4
|
+
``writes=True`` so the permission gate prompts with the exact command as
|
|
5
|
+
preview. Checkpointing only covers file tools, so git actions rely on the
|
|
6
|
+
approval prompt instead.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from kiwimatecoder import git as git_module
|
|
14
|
+
from kiwimatecoder.session import Session
|
|
15
|
+
from kiwimatecoder.tools.base import FunctionTool, ToolResult
|
|
16
|
+
|
|
17
|
+
READ_ACTIONS = ("status", "diff", "log", "show", "branches")
|
|
18
|
+
WRITE_ACTIONS = git_module.WRITE_ACTIONS
|
|
19
|
+
|
|
20
|
+
_LOG_LIMIT_DEFAULT = 20
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _truncate(text: str) -> str:
|
|
24
|
+
if len(text) > git_module.MAX_OUTPUT:
|
|
25
|
+
return text[: git_module.MAX_OUTPUT] + "\n... [output truncated]"
|
|
26
|
+
return text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _render(code: int, output: str) -> ToolResult:
|
|
30
|
+
output = output.strip()
|
|
31
|
+
body = output if output else "(no output)"
|
|
32
|
+
content = f"{_truncate(body)}\n[exit code: {code}]"
|
|
33
|
+
return ToolResult(content=content, ok=code == 0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _git(args: dict[str, Any], session: Session) -> ToolResult:
|
|
37
|
+
action = str(args.get("action") or "status").strip().lower()
|
|
38
|
+
if action not in READ_ACTIONS:
|
|
39
|
+
return ToolResult.error(
|
|
40
|
+
f"Unknown git action '{action}'. Choose: {', '.join(READ_ACTIONS)}."
|
|
41
|
+
)
|
|
42
|
+
path = args.get("path")
|
|
43
|
+
path_text = str(path).strip() if path is not None else None
|
|
44
|
+
ref = args.get("ref")
|
|
45
|
+
ref_text = str(ref).strip() if ref is not None else None
|
|
46
|
+
|
|
47
|
+
if action == "status":
|
|
48
|
+
command = git_module.status_args()
|
|
49
|
+
elif action == "diff":
|
|
50
|
+
command = git_module.diff_args(
|
|
51
|
+
staged=bool(args.get("staged", False)), path=path_text, ref=ref_text
|
|
52
|
+
)
|
|
53
|
+
elif action == "log":
|
|
54
|
+
raw_limit = args.get("limit")
|
|
55
|
+
try:
|
|
56
|
+
limit = int(raw_limit) if raw_limit is not None else _LOG_LIMIT_DEFAULT
|
|
57
|
+
except (TypeError, ValueError):
|
|
58
|
+
return ToolResult.error("'limit' must be an integer")
|
|
59
|
+
command = git_module.log_args(limit, path=path_text, oneline=True)
|
|
60
|
+
elif action == "show":
|
|
61
|
+
command = git_module.show_args(ref_text or "HEAD")
|
|
62
|
+
else:
|
|
63
|
+
command = git_module.branch_args()
|
|
64
|
+
|
|
65
|
+
code, output = git_module.run_git(command, session)
|
|
66
|
+
return _render(code, output)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _git_write(args: dict[str, Any], session: Session) -> ToolResult:
|
|
70
|
+
action = str(args.get("action") or "").strip().lower()
|
|
71
|
+
if action not in WRITE_ACTIONS:
|
|
72
|
+
return ToolResult.error(
|
|
73
|
+
f"Unknown git_write action '{action}'. Choose: {', '.join(WRITE_ACTIONS)}."
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
command = git_module.build_write_args(args)
|
|
77
|
+
except ValueError as exc:
|
|
78
|
+
return ToolResult.error(str(exc))
|
|
79
|
+
|
|
80
|
+
code, output = git_module.run_git(command, session, timeout=60)
|
|
81
|
+
return _render(code, output)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
git_tool = FunctionTool(
|
|
85
|
+
name="git",
|
|
86
|
+
description=(
|
|
87
|
+
"Inspect the repository (read-only): status, diff (staged or not, "
|
|
88
|
+
"optionally against a ref), log (1-100 commits), show a ref, or list "
|
|
89
|
+
"branches. Use it before staging or committing to check what changed."
|
|
90
|
+
),
|
|
91
|
+
parameters={
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"action": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"enum": list(READ_ACTIONS),
|
|
97
|
+
"description": "Which read-only git command to run.",
|
|
98
|
+
},
|
|
99
|
+
"path": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "Limit diff/log to this path.",
|
|
102
|
+
},
|
|
103
|
+
"ref": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Commit/branch for diff or show (defaults to HEAD).",
|
|
106
|
+
},
|
|
107
|
+
"staged": {
|
|
108
|
+
"type": "boolean",
|
|
109
|
+
"description": "Diff the staged changes instead of the working tree.",
|
|
110
|
+
},
|
|
111
|
+
"limit": {
|
|
112
|
+
"type": "integer",
|
|
113
|
+
"description": "Number of log entries (1-100, default 20).",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"required": ["action"],
|
|
117
|
+
},
|
|
118
|
+
func=_git,
|
|
119
|
+
writes=False,
|
|
120
|
+
runs=False,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
git_write_tool = FunctionTool(
|
|
124
|
+
name="git_write",
|
|
125
|
+
description=(
|
|
126
|
+
"Make a git change: stage paths, unstage paths, commit with a message, "
|
|
127
|
+
"or create and switch to a new branch. Requires user approval. Push, "
|
|
128
|
+
"reset, and clean are intentionally unavailable."
|
|
129
|
+
),
|
|
130
|
+
parameters={
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"action": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"enum": list(WRITE_ACTIONS),
|
|
136
|
+
"description": "Which git mutation to perform.",
|
|
137
|
+
},
|
|
138
|
+
"paths": {
|
|
139
|
+
"type": "array",
|
|
140
|
+
"items": {"type": "string"},
|
|
141
|
+
"description": "Paths to stage or unstage.",
|
|
142
|
+
},
|
|
143
|
+
"message": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "Commit message (required for commit).",
|
|
146
|
+
},
|
|
147
|
+
"branch": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"description": "New branch name (required for checkout_branch).",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
"required": ["action"],
|
|
153
|
+
},
|
|
154
|
+
func=_git_write,
|
|
155
|
+
writes=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
preview = git_module.preview
|