llmcode-cli 1.0.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.
- llm_code/__init__.py +2 -0
- llm_code/analysis/__init__.py +6 -0
- llm_code/analysis/cache.py +33 -0
- llm_code/analysis/engine.py +256 -0
- llm_code/analysis/go_rules.py +114 -0
- llm_code/analysis/js_rules.py +84 -0
- llm_code/analysis/python_rules.py +311 -0
- llm_code/analysis/rules.py +140 -0
- llm_code/analysis/rust_rules.py +108 -0
- llm_code/analysis/universal_rules.py +111 -0
- llm_code/api/__init__.py +0 -0
- llm_code/api/client.py +90 -0
- llm_code/api/errors.py +73 -0
- llm_code/api/openai_compat.py +390 -0
- llm_code/api/provider.py +35 -0
- llm_code/api/sse.py +52 -0
- llm_code/api/types.py +140 -0
- llm_code/cli/__init__.py +0 -0
- llm_code/cli/commands.py +70 -0
- llm_code/cli/image.py +122 -0
- llm_code/cli/render.py +214 -0
- llm_code/cli/status_line.py +79 -0
- llm_code/cli/streaming.py +92 -0
- llm_code/cli/tui_main.py +220 -0
- llm_code/computer_use/__init__.py +11 -0
- llm_code/computer_use/app_detect.py +49 -0
- llm_code/computer_use/app_tier.py +57 -0
- llm_code/computer_use/coordinator.py +99 -0
- llm_code/computer_use/input_control.py +71 -0
- llm_code/computer_use/screenshot.py +93 -0
- llm_code/cron/__init__.py +13 -0
- llm_code/cron/parser.py +145 -0
- llm_code/cron/scheduler.py +135 -0
- llm_code/cron/storage.py +126 -0
- llm_code/enterprise/__init__.py +1 -0
- llm_code/enterprise/audit.py +59 -0
- llm_code/enterprise/auth.py +26 -0
- llm_code/enterprise/oidc.py +95 -0
- llm_code/enterprise/rbac.py +65 -0
- llm_code/harness/__init__.py +5 -0
- llm_code/harness/config.py +33 -0
- llm_code/harness/engine.py +129 -0
- llm_code/harness/guides.py +41 -0
- llm_code/harness/sensors.py +68 -0
- llm_code/harness/templates.py +84 -0
- llm_code/hida/__init__.py +1 -0
- llm_code/hida/classifier.py +187 -0
- llm_code/hida/engine.py +49 -0
- llm_code/hida/profiles.py +95 -0
- llm_code/hida/types.py +28 -0
- llm_code/ide/__init__.py +1 -0
- llm_code/ide/bridge.py +80 -0
- llm_code/ide/detector.py +76 -0
- llm_code/ide/server.py +169 -0
- llm_code/logging.py +29 -0
- llm_code/lsp/__init__.py +0 -0
- llm_code/lsp/client.py +298 -0
- llm_code/lsp/detector.py +42 -0
- llm_code/lsp/manager.py +56 -0
- llm_code/lsp/tools.py +288 -0
- llm_code/marketplace/__init__.py +0 -0
- llm_code/marketplace/builtin_registry.py +102 -0
- llm_code/marketplace/installer.py +162 -0
- llm_code/marketplace/plugin.py +78 -0
- llm_code/marketplace/registry.py +360 -0
- llm_code/mcp/__init__.py +0 -0
- llm_code/mcp/bridge.py +87 -0
- llm_code/mcp/client.py +117 -0
- llm_code/mcp/health.py +120 -0
- llm_code/mcp/manager.py +214 -0
- llm_code/mcp/oauth.py +219 -0
- llm_code/mcp/transport.py +254 -0
- llm_code/mcp/types.py +53 -0
- llm_code/remote/__init__.py +0 -0
- llm_code/remote/client.py +136 -0
- llm_code/remote/protocol.py +22 -0
- llm_code/remote/server.py +275 -0
- llm_code/remote/ssh_proxy.py +56 -0
- llm_code/runtime/__init__.py +0 -0
- llm_code/runtime/auto_commit.py +56 -0
- llm_code/runtime/auto_diagnose.py +62 -0
- llm_code/runtime/checkpoint.py +70 -0
- llm_code/runtime/checkpoint_recovery.py +142 -0
- llm_code/runtime/compaction.py +35 -0
- llm_code/runtime/compressor.py +415 -0
- llm_code/runtime/config.py +533 -0
- llm_code/runtime/context.py +49 -0
- llm_code/runtime/conversation.py +921 -0
- llm_code/runtime/cost_tracker.py +126 -0
- llm_code/runtime/dream.py +127 -0
- llm_code/runtime/file_protection.py +150 -0
- llm_code/runtime/hardware.py +85 -0
- llm_code/runtime/hooks.py +223 -0
- llm_code/runtime/indexer.py +230 -0
- llm_code/runtime/knowledge_compiler.py +232 -0
- llm_code/runtime/memory.py +132 -0
- llm_code/runtime/memory_layers.py +467 -0
- llm_code/runtime/memory_lint.py +252 -0
- llm_code/runtime/model_aliases.py +37 -0
- llm_code/runtime/ollama.py +93 -0
- llm_code/runtime/overlay.py +124 -0
- llm_code/runtime/permissions.py +200 -0
- llm_code/runtime/plan.py +45 -0
- llm_code/runtime/prompt.py +238 -0
- llm_code/runtime/repo_map.py +174 -0
- llm_code/runtime/sandbox.py +116 -0
- llm_code/runtime/session.py +268 -0
- llm_code/runtime/skill_resolver.py +61 -0
- llm_code/runtime/skills.py +133 -0
- llm_code/runtime/speculative.py +75 -0
- llm_code/runtime/streaming_executor.py +216 -0
- llm_code/runtime/telemetry.py +196 -0
- llm_code/runtime/token_budget.py +26 -0
- llm_code/runtime/vcr.py +142 -0
- llm_code/runtime/vision.py +102 -0
- llm_code/swarm/__init__.py +1 -0
- llm_code/swarm/backend_subprocess.py +108 -0
- llm_code/swarm/backend_tmux.py +103 -0
- llm_code/swarm/backend_worktree.py +306 -0
- llm_code/swarm/checkpoint.py +74 -0
- llm_code/swarm/coordinator.py +236 -0
- llm_code/swarm/mailbox.py +88 -0
- llm_code/swarm/manager.py +202 -0
- llm_code/swarm/memory_sync.py +80 -0
- llm_code/swarm/recovery.py +21 -0
- llm_code/swarm/team.py +67 -0
- llm_code/swarm/types.py +31 -0
- llm_code/task/__init__.py +16 -0
- llm_code/task/diagnostics.py +93 -0
- llm_code/task/manager.py +162 -0
- llm_code/task/types.py +112 -0
- llm_code/task/verifier.py +104 -0
- llm_code/tools/__init__.py +0 -0
- llm_code/tools/agent.py +145 -0
- llm_code/tools/agent_roles.py +82 -0
- llm_code/tools/base.py +94 -0
- llm_code/tools/bash.py +565 -0
- llm_code/tools/computer_use_tools.py +278 -0
- llm_code/tools/coordinator_tool.py +75 -0
- llm_code/tools/cron_create.py +90 -0
- llm_code/tools/cron_delete.py +49 -0
- llm_code/tools/cron_list.py +51 -0
- llm_code/tools/deferred.py +92 -0
- llm_code/tools/dump.py +116 -0
- llm_code/tools/edit_file.py +282 -0
- llm_code/tools/git_tools.py +531 -0
- llm_code/tools/glob_search.py +112 -0
- llm_code/tools/grep_search.py +144 -0
- llm_code/tools/ide_diagnostics.py +59 -0
- llm_code/tools/ide_open.py +58 -0
- llm_code/tools/ide_selection.py +52 -0
- llm_code/tools/memory_tools.py +138 -0
- llm_code/tools/multi_edit.py +143 -0
- llm_code/tools/notebook_edit.py +107 -0
- llm_code/tools/notebook_read.py +81 -0
- llm_code/tools/parsing.py +63 -0
- llm_code/tools/read_file.py +154 -0
- llm_code/tools/registry.py +58 -0
- llm_code/tools/search_backends/__init__.py +56 -0
- llm_code/tools/search_backends/brave.py +56 -0
- llm_code/tools/search_backends/duckduckgo.py +129 -0
- llm_code/tools/search_backends/searxng.py +71 -0
- llm_code/tools/search_backends/tavily.py +73 -0
- llm_code/tools/swarm_create.py +109 -0
- llm_code/tools/swarm_delete.py +95 -0
- llm_code/tools/swarm_list.py +44 -0
- llm_code/tools/swarm_message.py +109 -0
- llm_code/tools/task_close.py +79 -0
- llm_code/tools/task_plan.py +79 -0
- llm_code/tools/task_verify.py +90 -0
- llm_code/tools/tool_search.py +65 -0
- llm_code/tools/web_common.py +258 -0
- llm_code/tools/web_fetch.py +223 -0
- llm_code/tools/web_search.py +280 -0
- llm_code/tools/write_file.py +118 -0
- llm_code/tui/__init__.py +1 -0
- llm_code/tui/app.py +2432 -0
- llm_code/tui/chat_view.py +82 -0
- llm_code/tui/chat_widgets.py +309 -0
- llm_code/tui/header_bar.py +46 -0
- llm_code/tui/input_bar.py +349 -0
- llm_code/tui/keybindings.py +142 -0
- llm_code/tui/marketplace.py +210 -0
- llm_code/tui/status_bar.py +72 -0
- llm_code/tui/theme.py +96 -0
- llm_code/utils/__init__.py +0 -0
- llm_code/utils/diff.py +111 -0
- llm_code/utils/errors.py +70 -0
- llm_code/utils/hyperlink.py +73 -0
- llm_code/utils/notebook.py +179 -0
- llm_code/utils/search.py +69 -0
- llm_code/utils/text_normalize.py +28 -0
- llm_code/utils/version_check.py +62 -0
- llm_code/vim/__init__.py +4 -0
- llm_code/vim/engine.py +51 -0
- llm_code/vim/motions.py +172 -0
- llm_code/vim/operators.py +183 -0
- llm_code/vim/text_objects.py +139 -0
- llm_code/vim/transitions.py +279 -0
- llm_code/vim/types.py +68 -0
- llm_code/voice/__init__.py +1 -0
- llm_code/voice/languages.py +43 -0
- llm_code/voice/recorder.py +136 -0
- llm_code/voice/stt.py +36 -0
- llm_code/voice/stt_anthropic.py +66 -0
- llm_code/voice/stt_google.py +32 -0
- llm_code/voice/stt_whisper.py +52 -0
- llmcode_cli-1.0.0.dist-info/METADATA +524 -0
- llmcode_cli-1.0.0.dist-info/RECORD +212 -0
- llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
- llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
- llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""Git-aware tools for interacting with a local git repository."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import fnmatch
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolResult
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Sensitive file patterns — block these in GitCommitTool
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
_SENSITIVE_PATTERNS: list[str] = [
|
|
17
|
+
".env",
|
|
18
|
+
".env.*",
|
|
19
|
+
"*.key",
|
|
20
|
+
"*.pem",
|
|
21
|
+
"*.p12",
|
|
22
|
+
"credentials.*",
|
|
23
|
+
"secret*",
|
|
24
|
+
"*_secret*",
|
|
25
|
+
"*.credential",
|
|
26
|
+
"token.json",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_sensitive(filename: str) -> bool:
|
|
31
|
+
"""Return True if filename matches any sensitive pattern."""
|
|
32
|
+
basename = os.path.basename(filename)
|
|
33
|
+
return any(fnmatch.fnmatch(basename, pattern) for pattern in _SENSITIVE_PATTERNS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _auto_stash(cwd: "str | None" = None) -> bool:
|
|
37
|
+
"""Stash uncommitted changes if any exist.
|
|
38
|
+
|
|
39
|
+
Returns True if a stash was created, False if the working tree was clean.
|
|
40
|
+
"""
|
|
41
|
+
if cwd is None:
|
|
42
|
+
cwd = os.getcwd()
|
|
43
|
+
status = subprocess.run(
|
|
44
|
+
["git", "status", "--porcelain"],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
cwd=cwd,
|
|
48
|
+
)
|
|
49
|
+
if status.returncode != 0 or not status.stdout.strip():
|
|
50
|
+
return False
|
|
51
|
+
stash = subprocess.run(
|
|
52
|
+
["git", "stash", "push", "-m", "llm-code auto-stash"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
)
|
|
57
|
+
return stash.returncode == 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _auto_unstash(cwd: "str | None" = None) -> None:
|
|
61
|
+
"""Restore the most recent stash (used after auto-stash)."""
|
|
62
|
+
if cwd is None:
|
|
63
|
+
cwd = os.getcwd()
|
|
64
|
+
subprocess.run(
|
|
65
|
+
["git", "stash", "pop"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
cwd=cwd,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _run_git(args: list[str], cwd: str | None = None) -> ToolResult:
|
|
73
|
+
"""Run a git command and return a ToolResult."""
|
|
74
|
+
if cwd is None:
|
|
75
|
+
cwd = os.getcwd()
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["git"] + args,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
cwd=cwd,
|
|
81
|
+
)
|
|
82
|
+
output = result.stdout
|
|
83
|
+
if result.returncode != 0:
|
|
84
|
+
# Combine stderr into output for diagnostics
|
|
85
|
+
output = (result.stderr or result.stdout).strip()
|
|
86
|
+
return ToolResult(output=output, is_error=True)
|
|
87
|
+
return ToolResult(output=output.rstrip("\n"), is_error=False)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Input models
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class GitDiffInput(BaseModel):
|
|
96
|
+
path: str = ""
|
|
97
|
+
staged: bool = False
|
|
98
|
+
commit: str = ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class GitLogInput(BaseModel):
|
|
102
|
+
limit: int = 10
|
|
103
|
+
oneline: bool = True
|
|
104
|
+
path: str = ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class GitCommitInput(BaseModel):
|
|
108
|
+
message: str
|
|
109
|
+
files: list[str] = []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class GitPushInput(BaseModel):
|
|
113
|
+
remote: str = "origin"
|
|
114
|
+
branch: str = ""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class GitStashInput(BaseModel):
|
|
118
|
+
action: str # push / pop / list
|
|
119
|
+
message: str = ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class GitBranchInput(BaseModel):
|
|
123
|
+
action: str # list / create / switch / delete
|
|
124
|
+
name: str = ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# 1. GitStatusTool
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class GitStatusTool(Tool):
|
|
133
|
+
@property
|
|
134
|
+
def name(self) -> str:
|
|
135
|
+
return "git_status"
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def description(self) -> str:
|
|
139
|
+
return "Show the working-tree status in short format."
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def input_schema(self) -> dict:
|
|
143
|
+
return {"type": "object", "properties": {}, "required": []}
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def required_permission(self) -> PermissionLevel:
|
|
147
|
+
return PermissionLevel.READ_ONLY
|
|
148
|
+
|
|
149
|
+
def is_read_only(self, args: dict) -> bool:
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def execute(self, args: dict) -> ToolResult:
|
|
156
|
+
return _run_git(["status", "--short"])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# 2. GitDiffTool
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class GitDiffTool(Tool):
|
|
165
|
+
@property
|
|
166
|
+
def name(self) -> str:
|
|
167
|
+
return "git_diff"
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def description(self) -> str:
|
|
171
|
+
return "Show changes between commits, working tree, or index."
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def input_schema(self) -> dict:
|
|
175
|
+
return {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {
|
|
178
|
+
"path": {"type": "string", "default": ""},
|
|
179
|
+
"staged": {"type": "boolean", "default": False},
|
|
180
|
+
"commit": {"type": "string", "default": ""},
|
|
181
|
+
},
|
|
182
|
+
"required": [],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def required_permission(self) -> PermissionLevel:
|
|
187
|
+
return PermissionLevel.READ_ONLY
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def input_model(self) -> type[GitDiffInput]:
|
|
191
|
+
return GitDiffInput
|
|
192
|
+
|
|
193
|
+
def is_read_only(self, args: dict) -> bool:
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
def execute(self, args: dict) -> ToolResult:
|
|
200
|
+
cmd: list[str] = ["diff"]
|
|
201
|
+
staged: bool = args.get("staged", False)
|
|
202
|
+
commit: str = args.get("commit", "")
|
|
203
|
+
path: str = args.get("path", "")
|
|
204
|
+
|
|
205
|
+
if staged:
|
|
206
|
+
cmd.append("--staged")
|
|
207
|
+
if commit:
|
|
208
|
+
cmd.append(commit)
|
|
209
|
+
if path:
|
|
210
|
+
cmd += ["--", path]
|
|
211
|
+
|
|
212
|
+
return _run_git(cmd)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# 3. GitLogTool
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class GitLogTool(Tool):
|
|
221
|
+
@property
|
|
222
|
+
def name(self) -> str:
|
|
223
|
+
return "git_log"
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def description(self) -> str:
|
|
227
|
+
return "Show the commit log."
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def input_schema(self) -> dict:
|
|
231
|
+
return {
|
|
232
|
+
"type": "object",
|
|
233
|
+
"properties": {
|
|
234
|
+
"limit": {"type": "integer", "default": 10},
|
|
235
|
+
"oneline": {"type": "boolean", "default": True},
|
|
236
|
+
"path": {"type": "string", "default": ""},
|
|
237
|
+
},
|
|
238
|
+
"required": [],
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def required_permission(self) -> PermissionLevel:
|
|
243
|
+
return PermissionLevel.READ_ONLY
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def input_model(self) -> type[GitLogInput]:
|
|
247
|
+
return GitLogInput
|
|
248
|
+
|
|
249
|
+
def is_read_only(self, args: dict) -> bool:
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
def execute(self, args: dict) -> ToolResult:
|
|
256
|
+
limit: int = int(args.get("limit", 10))
|
|
257
|
+
oneline: bool = args.get("oneline", True)
|
|
258
|
+
path: str = args.get("path", "")
|
|
259
|
+
|
|
260
|
+
cmd: list[str] = ["log", f"-n{limit}"]
|
|
261
|
+
if oneline:
|
|
262
|
+
cmd.append("--oneline")
|
|
263
|
+
if path:
|
|
264
|
+
cmd += ["--", path]
|
|
265
|
+
|
|
266
|
+
return _run_git(cmd)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# 4. GitCommitTool
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class GitCommitTool(Tool):
|
|
275
|
+
@property
|
|
276
|
+
def name(self) -> str:
|
|
277
|
+
return "git_commit"
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def description(self) -> str:
|
|
281
|
+
return "Stage files and create a git commit."
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def input_schema(self) -> dict:
|
|
285
|
+
return {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"properties": {
|
|
288
|
+
"message": {"type": "string"},
|
|
289
|
+
"files": {
|
|
290
|
+
"type": "array",
|
|
291
|
+
"items": {"type": "string"},
|
|
292
|
+
"default": [],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
"required": ["message"],
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def required_permission(self) -> PermissionLevel:
|
|
300
|
+
return PermissionLevel.WORKSPACE_WRITE
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def input_model(self) -> type[GitCommitInput]:
|
|
304
|
+
return GitCommitInput
|
|
305
|
+
|
|
306
|
+
def is_read_only(self, args: dict) -> bool:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def is_destructive(self, args: dict) -> bool:
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
def execute(self, args: dict) -> ToolResult:
|
|
313
|
+
message: str = args["message"]
|
|
314
|
+
files: list[str] = args.get("files", [])
|
|
315
|
+
|
|
316
|
+
# Safety: reject sensitive files
|
|
317
|
+
sensitive = [f for f in files if _is_sensitive(f)]
|
|
318
|
+
if sensitive:
|
|
319
|
+
return ToolResult(
|
|
320
|
+
output=f"Blocked: sensitive file(s) detected: {', '.join(sensitive)}",
|
|
321
|
+
is_error=True,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
cwd = os.getcwd()
|
|
325
|
+
|
|
326
|
+
# Stage files
|
|
327
|
+
if files:
|
|
328
|
+
add_result = subprocess.run(
|
|
329
|
+
["git", "add"] + files,
|
|
330
|
+
capture_output=True,
|
|
331
|
+
text=True,
|
|
332
|
+
cwd=cwd,
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
add_result = subprocess.run(
|
|
336
|
+
["git", "add", "-A"],
|
|
337
|
+
capture_output=True,
|
|
338
|
+
text=True,
|
|
339
|
+
cwd=cwd,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if add_result.returncode != 0:
|
|
343
|
+
return ToolResult(
|
|
344
|
+
output=(add_result.stderr or add_result.stdout).strip(),
|
|
345
|
+
is_error=True,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
commit_result = subprocess.run(
|
|
349
|
+
["git", "commit", "-m", message],
|
|
350
|
+
capture_output=True,
|
|
351
|
+
text=True,
|
|
352
|
+
cwd=cwd,
|
|
353
|
+
)
|
|
354
|
+
if commit_result.returncode != 0:
|
|
355
|
+
return ToolResult(
|
|
356
|
+
output=(commit_result.stderr or commit_result.stdout).strip(),
|
|
357
|
+
is_error=True,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return ToolResult(output=commit_result.stdout.rstrip("\n"), is_error=False)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
# 5. GitPushTool
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class GitPushTool(Tool):
|
|
369
|
+
@property
|
|
370
|
+
def name(self) -> str:
|
|
371
|
+
return "git_push"
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def description(self) -> str:
|
|
375
|
+
return "Push commits to a remote repository."
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def input_schema(self) -> dict:
|
|
379
|
+
return {
|
|
380
|
+
"type": "object",
|
|
381
|
+
"properties": {
|
|
382
|
+
"remote": {"type": "string", "default": "origin"},
|
|
383
|
+
"branch": {"type": "string", "default": ""},
|
|
384
|
+
},
|
|
385
|
+
"required": [],
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def required_permission(self) -> PermissionLevel:
|
|
390
|
+
return PermissionLevel.FULL_ACCESS
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def input_model(self) -> type[GitPushInput]:
|
|
394
|
+
return GitPushInput
|
|
395
|
+
|
|
396
|
+
def is_read_only(self, args: dict) -> bool:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
def is_destructive(self, args: dict) -> bool:
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
def execute(self, args: dict) -> ToolResult:
|
|
403
|
+
remote: str = args.get("remote", "origin")
|
|
404
|
+
branch: str = args.get("branch", "")
|
|
405
|
+
|
|
406
|
+
cmd: list[str] = ["push", remote]
|
|
407
|
+
if branch:
|
|
408
|
+
cmd.append(branch)
|
|
409
|
+
|
|
410
|
+
return _run_git(cmd)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
# 6. GitStashTool
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class GitStashTool(Tool):
|
|
419
|
+
@property
|
|
420
|
+
def name(self) -> str:
|
|
421
|
+
return "git_stash"
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def description(self) -> str:
|
|
425
|
+
return "Stash or restore changes in the working directory."
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def input_schema(self) -> dict:
|
|
429
|
+
return {
|
|
430
|
+
"type": "object",
|
|
431
|
+
"properties": {
|
|
432
|
+
"action": {"type": "string", "enum": ["push", "pop", "list"]},
|
|
433
|
+
"message": {"type": "string", "default": ""},
|
|
434
|
+
},
|
|
435
|
+
"required": ["action"],
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def required_permission(self) -> PermissionLevel:
|
|
440
|
+
return PermissionLevel.WORKSPACE_WRITE
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def input_model(self) -> type[GitStashInput]:
|
|
444
|
+
return GitStashInput
|
|
445
|
+
|
|
446
|
+
def execute(self, args: dict) -> ToolResult:
|
|
447
|
+
action: str = args["action"]
|
|
448
|
+
message: str = args.get("message", "")
|
|
449
|
+
|
|
450
|
+
if action == "push":
|
|
451
|
+
cmd = ["stash", "push"]
|
|
452
|
+
if message:
|
|
453
|
+
cmd += ["-m", message]
|
|
454
|
+
elif action == "pop":
|
|
455
|
+
cmd = ["stash", "pop"]
|
|
456
|
+
elif action == "list":
|
|
457
|
+
cmd = ["stash", "list"]
|
|
458
|
+
else:
|
|
459
|
+
return ToolResult(
|
|
460
|
+
output=f"Unknown stash action: {action!r}. Use push, pop, or list.",
|
|
461
|
+
is_error=True,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
return _run_git(cmd)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# ---------------------------------------------------------------------------
|
|
468
|
+
# 7. GitBranchTool
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class GitBranchTool(Tool):
|
|
473
|
+
@property
|
|
474
|
+
def name(self) -> str:
|
|
475
|
+
return "git_branch"
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def description(self) -> str:
|
|
479
|
+
return "List, create, switch, or delete git branches."
|
|
480
|
+
|
|
481
|
+
@property
|
|
482
|
+
def input_schema(self) -> dict:
|
|
483
|
+
return {
|
|
484
|
+
"type": "object",
|
|
485
|
+
"properties": {
|
|
486
|
+
"action": {"type": "string", "enum": ["list", "create", "switch", "delete"]},
|
|
487
|
+
"name": {"type": "string", "default": ""},
|
|
488
|
+
},
|
|
489
|
+
"required": ["action"],
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def required_permission(self) -> PermissionLevel:
|
|
494
|
+
return PermissionLevel.WORKSPACE_WRITE
|
|
495
|
+
|
|
496
|
+
@property
|
|
497
|
+
def input_model(self) -> type[GitBranchInput]:
|
|
498
|
+
return GitBranchInput
|
|
499
|
+
|
|
500
|
+
def is_destructive(self, args: dict) -> bool:
|
|
501
|
+
return args.get("action") == "delete"
|
|
502
|
+
|
|
503
|
+
def execute(self, args: dict) -> ToolResult:
|
|
504
|
+
action: str = args["action"]
|
|
505
|
+
name: str = args.get("name", "")
|
|
506
|
+
|
|
507
|
+
if action == "list":
|
|
508
|
+
cmd = ["branch", "-a"]
|
|
509
|
+
elif action == "create":
|
|
510
|
+
if not name:
|
|
511
|
+
return ToolResult(output="Branch name required for create.", is_error=True)
|
|
512
|
+
cmd = ["checkout", "-b", name]
|
|
513
|
+
elif action == "switch":
|
|
514
|
+
if not name:
|
|
515
|
+
return ToolResult(output="Branch name required for switch.", is_error=True)
|
|
516
|
+
stashed = _auto_stash()
|
|
517
|
+
result = _run_git(["checkout", name])
|
|
518
|
+
if stashed:
|
|
519
|
+
_auto_unstash()
|
|
520
|
+
return result
|
|
521
|
+
elif action == "delete":
|
|
522
|
+
if not name:
|
|
523
|
+
return ToolResult(output="Branch name required for delete.", is_error=True)
|
|
524
|
+
cmd = ["branch", "-d", name]
|
|
525
|
+
else:
|
|
526
|
+
return ToolResult(
|
|
527
|
+
output=f"Unknown branch action: {action!r}. Use list, create, switch, or delete.",
|
|
528
|
+
is_error=True,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return _run_git(cmd)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""GlobSearchTool — find files matching a glob pattern, sorted by mtime."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from llm_code.tools.base import PermissionLevel, Tool, ToolProgress, ToolResult
|
|
10
|
+
|
|
11
|
+
_MAX_RESULTS = 100
|
|
12
|
+
_PROGRESS_INTERVAL = 50 # emit a progress event every N files scanned
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GlobSearchInput(BaseModel):
|
|
16
|
+
pattern: str
|
|
17
|
+
path: str = "."
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GlobSearchTool(Tool):
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "glob_search"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def description(self) -> str:
|
|
27
|
+
return (
|
|
28
|
+
"Search for files matching a glob pattern. "
|
|
29
|
+
"Returns up to 100 results sorted by modification time (newest first)."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def input_schema(self) -> dict:
|
|
34
|
+
return {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"pattern": {"type": "string", "description": "Glob pattern (e.g. **/*.py)"},
|
|
38
|
+
"path": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Directory to search in (default: current dir)",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
"required": ["pattern"],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def required_permission(self) -> PermissionLevel:
|
|
48
|
+
return PermissionLevel.READ_ONLY
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def input_model(self) -> type[GlobSearchInput]:
|
|
52
|
+
return GlobSearchInput
|
|
53
|
+
|
|
54
|
+
def is_read_only(self, args: dict) -> bool:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
def is_concurrency_safe(self, args: dict) -> bool:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def execute(self, args: dict) -> ToolResult:
|
|
61
|
+
pattern: str = args["pattern"]
|
|
62
|
+
search_path = pathlib.Path(args.get("path", "."))
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
matches = list(search_path.glob(pattern))
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
return ToolResult(output=f"Glob error: {exc}", is_error=True)
|
|
68
|
+
|
|
69
|
+
# Sort by mtime descending (newest first)
|
|
70
|
+
matches.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
71
|
+
matches = matches[:_MAX_RESULTS]
|
|
72
|
+
|
|
73
|
+
if not matches:
|
|
74
|
+
return ToolResult(output=f"No files matched: {pattern}")
|
|
75
|
+
|
|
76
|
+
return ToolResult(output="\n".join(str(m) for m in matches))
|
|
77
|
+
|
|
78
|
+
def execute_with_progress(
|
|
79
|
+
self,
|
|
80
|
+
args: dict,
|
|
81
|
+
on_progress: Callable[[ToolProgress], None],
|
|
82
|
+
) -> ToolResult:
|
|
83
|
+
pattern: str = args["pattern"]
|
|
84
|
+
search_path = pathlib.Path(args.get("path", "."))
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
all_matches = list(search_path.glob(pattern))
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
return ToolResult(output=f"Glob error: {exc}", is_error=True)
|
|
90
|
+
|
|
91
|
+
total = len(all_matches)
|
|
92
|
+
|
|
93
|
+
# Emit progress every PROGRESS_INTERVAL files
|
|
94
|
+
for i, _ in enumerate(all_matches, start=1):
|
|
95
|
+
if i % _PROGRESS_INTERVAL == 0:
|
|
96
|
+
percent = round(i / total * 100.0, 1) if total else 100.0
|
|
97
|
+
on_progress(
|
|
98
|
+
ToolProgress(
|
|
99
|
+
tool_name=self.name,
|
|
100
|
+
message=f"Scanned {i}/{total} files",
|
|
101
|
+
percent=percent,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Sort by mtime descending (newest first)
|
|
106
|
+
all_matches.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
107
|
+
matches = all_matches[:_MAX_RESULTS]
|
|
108
|
+
|
|
109
|
+
if not matches:
|
|
110
|
+
return ToolResult(output=f"No files matched: {pattern}")
|
|
111
|
+
|
|
112
|
+
return ToolResult(output="\n".join(str(m) for m in matches))
|