vtx-coding-agent 0.1.1__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.
- vtx/__init__.py +63 -0
- vtx/async_utils.py +40 -0
- vtx/builtin_skills/github/SKILL.md +139 -0
- vtx/builtin_skills/init/SKILL.md +74 -0
- vtx/builtin_skills/review/SKILL.md +73 -0
- vtx/builtin_skills/skill-builder/SKILL.md +133 -0
- vtx/cli.py +90 -0
- vtx/config.py +741 -0
- vtx/context/__init__.py +15 -0
- vtx/context/_xml.py +8 -0
- vtx/context/agent_mds.py +128 -0
- vtx/context/git.py +64 -0
- vtx/context/loader.py +41 -0
- vtx/context/skills.py +423 -0
- vtx/core/__init__.py +47 -0
- vtx/core/compaction.py +89 -0
- vtx/core/errors.py +17 -0
- vtx/core/handoff.py +51 -0
- vtx/core/scratchpad.py +54 -0
- vtx/core/types.py +197 -0
- vtx/defaults/__init__.py +0 -0
- vtx/defaults/config.yml +53 -0
- vtx/diff_display.py +12 -0
- vtx/events.py +224 -0
- vtx/gh_cli.py +82 -0
- vtx/git_branch.py +90 -0
- vtx/headless.py +127 -0
- vtx/llm/__init__.py +93 -0
- vtx/llm/base.py +217 -0
- vtx/llm/context_length.py +150 -0
- vtx/llm/dynamic_models.py +735 -0
- vtx/llm/model_fetcher.py +279 -0
- vtx/llm/models.py +78 -0
- vtx/llm/oauth/__init__.py +59 -0
- vtx/llm/oauth/copilot.py +358 -0
- vtx/llm/oauth/dynamic.py +236 -0
- vtx/llm/oauth/openai.py +400 -0
- vtx/llm/phase_parser.py +270 -0
- vtx/llm/provider.yaml +280 -0
- vtx/llm/provider_catalog.py +230 -0
- vtx/llm/providers/__init__.py +45 -0
- vtx/llm/providers/anthropic_sdk.py +256 -0
- vtx/llm/providers/mock.py +249 -0
- vtx/llm/providers/openai_sdk.py +246 -0
- vtx/llm/providers/sanitize.py +14 -0
- vtx/llm/sdk/__init__.py +13 -0
- vtx/llm/sdk/anthropic.py +382 -0
- vtx/llm/sdk/base.py +82 -0
- vtx/llm/sdk/openai.py +344 -0
- vtx/llm/tool_parser.py +161 -0
- vtx/loop.py +272 -0
- vtx/notify.py +109 -0
- vtx/permissions.py +114 -0
- vtx/prompts/__init__.py +45 -0
- vtx/prompts/builder.py +86 -0
- vtx/prompts/env.py +58 -0
- vtx/prompts/identity.py +166 -0
- vtx/prompts/tooling.py +36 -0
- vtx/py.typed +0 -0
- vtx/runtime.py +580 -0
- vtx/session.py +868 -0
- vtx/sounds/completion.wav +0 -0
- vtx/sounds/error.wav +0 -0
- vtx/sounds/permission.wav +0 -0
- vtx/themes.py +1104 -0
- vtx/tools/__init__.py +68 -0
- vtx/tools/_read_image.py +106 -0
- vtx/tools/_tool_utils.py +90 -0
- vtx/tools/base.py +36 -0
- vtx/tools/bash.py +371 -0
- vtx/tools/edit.py +261 -0
- vtx/tools/find.py +132 -0
- vtx/tools/read.py +238 -0
- vtx/tools/skill.py +278 -0
- vtx/tools/web.py +238 -0
- vtx/tools/write.py +88 -0
- vtx/tools_manager.py +216 -0
- vtx/turn.py +789 -0
- vtx/ui/__init__.py +0 -0
- vtx/ui/agent_runner.py +417 -0
- vtx/ui/app.py +665 -0
- vtx/ui/app_protocol.py +29 -0
- vtx/ui/autocomplete.py +440 -0
- vtx/ui/blocks.py +735 -0
- vtx/ui/chat.py +613 -0
- vtx/ui/clipboard.py +59 -0
- vtx/ui/commands/__init__.py +100 -0
- vtx/ui/commands/auth.py +306 -0
- vtx/ui/commands/base.py +122 -0
- vtx/ui/commands/models.py +144 -0
- vtx/ui/commands/sessions.py +388 -0
- vtx/ui/commands/settings.py +286 -0
- vtx/ui/completion_ui.py +313 -0
- vtx/ui/export.py +703 -0
- vtx/ui/floating_list.py +370 -0
- vtx/ui/formatting.py +287 -0
- vtx/ui/input.py +760 -0
- vtx/ui/latex.py +349 -0
- vtx/ui/launch.py +108 -0
- vtx/ui/path_complete.py +228 -0
- vtx/ui/prompt_history.py +102 -0
- vtx/ui/queue_ui.py +141 -0
- vtx/ui/selection_mode.py +18 -0
- vtx/ui/session_ui.py +235 -0
- vtx/ui/startup.py +124 -0
- vtx/ui/styles.py +327 -0
- vtx/ui/tool_output.py +34 -0
- vtx/ui/tree.py +437 -0
- vtx/ui/welcome.py +51 -0
- vtx/ui/widgets.py +558 -0
- vtx/update_check.py +49 -0
- vtx/version.py +22 -0
- vtx_coding_agent-0.1.1.dist-info/METADATA +259 -0
- vtx_coding_agent-0.1.1.dist-info/RECORD +117 -0
- vtx_coding_agent-0.1.1.dist-info/WHEEL +4 -0
- vtx_coding_agent-0.1.1.dist-info/entry_points.txt +2 -0
- vtx_coding_agent-0.1.1.dist-info/licenses/LICENSE +201 -0
vtx/tools_manager.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import platform
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import stat
|
|
6
|
+
import sys
|
|
7
|
+
import tarfile
|
|
8
|
+
import tempfile
|
|
9
|
+
import zipfile
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
import aiohttp
|
|
15
|
+
|
|
16
|
+
from .config import get_config_dir
|
|
17
|
+
|
|
18
|
+
ToolName = Literal["fd", "rg"]
|
|
19
|
+
|
|
20
|
+
_BIN_DIR = get_config_dir() / "bin"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class _ToolConfig:
|
|
25
|
+
name: str
|
|
26
|
+
repo: str
|
|
27
|
+
binary_name: str
|
|
28
|
+
tag_prefix: str
|
|
29
|
+
|
|
30
|
+
def get_asset_name(self, version: str, plat: str, arch: str) -> str | None:
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _FdConfig(_ToolConfig):
|
|
35
|
+
def get_asset_name(self, version: str, plat: str, arch: str) -> str | None:
|
|
36
|
+
if plat == "darwin":
|
|
37
|
+
arch_str = "aarch64" if arch == "arm64" else "x86_64"
|
|
38
|
+
return f"fd-v{version}-{arch_str}-apple-darwin.tar.gz"
|
|
39
|
+
elif plat == "linux":
|
|
40
|
+
arch_str = "aarch64" if arch == "arm64" else "x86_64"
|
|
41
|
+
return f"fd-v{version}-{arch_str}-unknown-linux-gnu.tar.gz"
|
|
42
|
+
elif plat == "win32":
|
|
43
|
+
arch_str = "aarch64" if arch == "arm64" else "x86_64"
|
|
44
|
+
return f"fd-v{version}-{arch_str}-pc-windows-msvc.zip"
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _RgConfig(_ToolConfig):
|
|
49
|
+
def get_asset_name(self, version: str, plat: str, arch: str) -> str | None:
|
|
50
|
+
if plat == "darwin":
|
|
51
|
+
arch_str = "aarch64" if arch == "arm64" else "x86_64"
|
|
52
|
+
return f"ripgrep-{version}-{arch_str}-apple-darwin.tar.gz"
|
|
53
|
+
elif plat == "linux":
|
|
54
|
+
if arch == "arm64":
|
|
55
|
+
return f"ripgrep-{version}-aarch64-unknown-linux-gnu.tar.gz"
|
|
56
|
+
return f"ripgrep-{version}-x86_64-unknown-linux-musl.tar.gz"
|
|
57
|
+
elif plat == "win32":
|
|
58
|
+
arch_str = "aarch64" if arch == "arm64" else "x86_64"
|
|
59
|
+
return f"ripgrep-{version}-{arch_str}-pc-windows-msvc.zip"
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_TOOLS: dict[ToolName, _ToolConfig] = {
|
|
64
|
+
"fd": _FdConfig(name="fd", repo="sharkdp/fd", binary_name="fd", tag_prefix="v"),
|
|
65
|
+
"rg": _RgConfig(name="ripgrep", repo="BurntSushi/ripgrep", binary_name="rg", tag_prefix=""),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_platform() -> str:
|
|
70
|
+
if sys.platform == "darwin":
|
|
71
|
+
return "darwin"
|
|
72
|
+
elif sys.platform == "win32":
|
|
73
|
+
return "win32"
|
|
74
|
+
return "linux"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_arch() -> str:
|
|
78
|
+
machine = platform.machine().lower()
|
|
79
|
+
if machine in ("arm64", "aarch64"):
|
|
80
|
+
return "arm64"
|
|
81
|
+
return "x86_64"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_tool_path(tool: ToolName) -> str | None:
|
|
85
|
+
config = _TOOLS.get(tool)
|
|
86
|
+
if not config:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
ext = ".exe" if _get_platform() == "win32" else ""
|
|
90
|
+
local_path = _BIN_DIR / (config.binary_name + ext)
|
|
91
|
+
if local_path.exists():
|
|
92
|
+
return str(local_path)
|
|
93
|
+
|
|
94
|
+
if tool == "fd":
|
|
95
|
+
return shutil.which("fd") or shutil.which("fdfind")
|
|
96
|
+
|
|
97
|
+
return shutil.which(config.binary_name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def _get_latest_version(session: aiohttp.ClientSession, repo: str) -> str:
|
|
101
|
+
async with session.get(
|
|
102
|
+
f"https://api.github.com/repos/{repo}/releases/latest", headers={"User-Agent": "vtx"}
|
|
103
|
+
) as resp:
|
|
104
|
+
resp.raise_for_status()
|
|
105
|
+
data = await resp.json()
|
|
106
|
+
version = data["tag_name"].removeprefix("v")
|
|
107
|
+
if not re.match(r"^\d+\.\d+(\.\d+)?$", version):
|
|
108
|
+
raise ValueError(f"Unexpected version format: {version}")
|
|
109
|
+
return version
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _download_file(session: aiohttp.ClientSession, url: str, dest: Path) -> None:
|
|
113
|
+
async with session.get(url) as resp:
|
|
114
|
+
resp.raise_for_status()
|
|
115
|
+
with open(dest, "wb") as f:
|
|
116
|
+
async for chunk in resp.content.iter_chunked(8192):
|
|
117
|
+
f.write(chunk)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _extract_binary(archive_path: Path, binary_name: str, dest: Path) -> Path:
|
|
121
|
+
ext = ".exe" if _get_platform() == "win32" else ""
|
|
122
|
+
target_binary = binary_name + ext
|
|
123
|
+
output_path = dest / target_binary
|
|
124
|
+
|
|
125
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
126
|
+
tmp = Path(tmp_dir)
|
|
127
|
+
|
|
128
|
+
if str(archive_path).endswith(".tar.gz"):
|
|
129
|
+
with tarfile.open(archive_path, "r:gz") as tar:
|
|
130
|
+
tar.extractall(tmp, filter="data")
|
|
131
|
+
elif str(archive_path).endswith(".zip"):
|
|
132
|
+
with zipfile.ZipFile(archive_path) as zf:
|
|
133
|
+
resolved_tmp = tmp.resolve()
|
|
134
|
+
for info in zf.infolist():
|
|
135
|
+
if not (tmp / info.filename).resolve().is_relative_to(resolved_tmp):
|
|
136
|
+
raise ValueError(f"Zip entry escapes target directory: {info.filename}")
|
|
137
|
+
zf.extractall(tmp)
|
|
138
|
+
|
|
139
|
+
# Search for the binary: could be at top level or in a subdirectory
|
|
140
|
+
candidates = list(tmp.rglob(target_binary))
|
|
141
|
+
if not candidates:
|
|
142
|
+
raise FileNotFoundError(f"Binary {target_binary} not found in archive")
|
|
143
|
+
|
|
144
|
+
shutil.move(str(candidates[0]), str(output_path))
|
|
145
|
+
|
|
146
|
+
if _get_platform() != "win32":
|
|
147
|
+
output_path.chmod(output_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
148
|
+
|
|
149
|
+
return output_path
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def _download_tool(tool: ToolName) -> str:
|
|
153
|
+
# TODO: Move archive extraction and other synchronous file operations
|
|
154
|
+
# off the event loop so background tool installation cannot cause UI hiccups.
|
|
155
|
+
config = _TOOLS[tool]
|
|
156
|
+
plat = _get_platform()
|
|
157
|
+
arch = _get_arch()
|
|
158
|
+
|
|
159
|
+
timeout = aiohttp.ClientTimeout(total=120)
|
|
160
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
161
|
+
version = await _get_latest_version(session, config.repo)
|
|
162
|
+
|
|
163
|
+
asset_name = config.get_asset_name(version, plat, arch)
|
|
164
|
+
if not asset_name:
|
|
165
|
+
raise RuntimeError(f"No binary available for {config.name} on {plat}/{arch}")
|
|
166
|
+
|
|
167
|
+
_BIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
|
|
169
|
+
download_url = (
|
|
170
|
+
f"https://github.com/{config.repo}/releases/download/"
|
|
171
|
+
f"{config.tag_prefix}{version}/{asset_name}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
archive_path = _BIN_DIR / asset_name
|
|
175
|
+
try:
|
|
176
|
+
await _download_file(session, download_url, archive_path)
|
|
177
|
+
binary_path = _extract_binary(archive_path, config.binary_name, _BIN_DIR)
|
|
178
|
+
return str(binary_path)
|
|
179
|
+
finally:
|
|
180
|
+
archive_path.unlink(missing_ok=True)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def ensure_tool(tool: ToolName, silent: bool = False) -> str | None:
|
|
184
|
+
existing = get_tool_path(tool)
|
|
185
|
+
if existing:
|
|
186
|
+
return existing
|
|
187
|
+
|
|
188
|
+
config = _TOOLS.get(tool)
|
|
189
|
+
if not config:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
asset_name = config.get_asset_name("0", _get_platform(), _get_arch())
|
|
193
|
+
if not asset_name:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
if not silent:
|
|
197
|
+
print(f"{config.name} not found. Downloading...", file=sys.stderr)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
path = await _download_tool(tool)
|
|
201
|
+
if not silent:
|
|
202
|
+
print(f"{config.name} installed to {path}", file=sys.stderr)
|
|
203
|
+
return path
|
|
204
|
+
except Exception as e:
|
|
205
|
+
if not silent:
|
|
206
|
+
print(f"Failed to download {config.name}: {e}", file=sys.stderr)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def ensure_tools(
|
|
211
|
+
tools: list[ToolName] | None = None, silent: bool = False
|
|
212
|
+
) -> dict[ToolName, str | None]:
|
|
213
|
+
if tools is None:
|
|
214
|
+
tools = ["fd", "rg"]
|
|
215
|
+
results = await asyncio.gather(*(ensure_tool(t, silent=silent) for t in tools))
|
|
216
|
+
return dict(zip(tools, results, strict=True))
|