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.
Files changed (117) hide show
  1. vtx/__init__.py +63 -0
  2. vtx/async_utils.py +40 -0
  3. vtx/builtin_skills/github/SKILL.md +139 -0
  4. vtx/builtin_skills/init/SKILL.md +74 -0
  5. vtx/builtin_skills/review/SKILL.md +73 -0
  6. vtx/builtin_skills/skill-builder/SKILL.md +133 -0
  7. vtx/cli.py +90 -0
  8. vtx/config.py +741 -0
  9. vtx/context/__init__.py +15 -0
  10. vtx/context/_xml.py +8 -0
  11. vtx/context/agent_mds.py +128 -0
  12. vtx/context/git.py +64 -0
  13. vtx/context/loader.py +41 -0
  14. vtx/context/skills.py +423 -0
  15. vtx/core/__init__.py +47 -0
  16. vtx/core/compaction.py +89 -0
  17. vtx/core/errors.py +17 -0
  18. vtx/core/handoff.py +51 -0
  19. vtx/core/scratchpad.py +54 -0
  20. vtx/core/types.py +197 -0
  21. vtx/defaults/__init__.py +0 -0
  22. vtx/defaults/config.yml +53 -0
  23. vtx/diff_display.py +12 -0
  24. vtx/events.py +224 -0
  25. vtx/gh_cli.py +82 -0
  26. vtx/git_branch.py +90 -0
  27. vtx/headless.py +127 -0
  28. vtx/llm/__init__.py +93 -0
  29. vtx/llm/base.py +217 -0
  30. vtx/llm/context_length.py +150 -0
  31. vtx/llm/dynamic_models.py +735 -0
  32. vtx/llm/model_fetcher.py +279 -0
  33. vtx/llm/models.py +78 -0
  34. vtx/llm/oauth/__init__.py +59 -0
  35. vtx/llm/oauth/copilot.py +358 -0
  36. vtx/llm/oauth/dynamic.py +236 -0
  37. vtx/llm/oauth/openai.py +400 -0
  38. vtx/llm/phase_parser.py +270 -0
  39. vtx/llm/provider.yaml +280 -0
  40. vtx/llm/provider_catalog.py +230 -0
  41. vtx/llm/providers/__init__.py +45 -0
  42. vtx/llm/providers/anthropic_sdk.py +256 -0
  43. vtx/llm/providers/mock.py +249 -0
  44. vtx/llm/providers/openai_sdk.py +246 -0
  45. vtx/llm/providers/sanitize.py +14 -0
  46. vtx/llm/sdk/__init__.py +13 -0
  47. vtx/llm/sdk/anthropic.py +382 -0
  48. vtx/llm/sdk/base.py +82 -0
  49. vtx/llm/sdk/openai.py +344 -0
  50. vtx/llm/tool_parser.py +161 -0
  51. vtx/loop.py +272 -0
  52. vtx/notify.py +109 -0
  53. vtx/permissions.py +114 -0
  54. vtx/prompts/__init__.py +45 -0
  55. vtx/prompts/builder.py +86 -0
  56. vtx/prompts/env.py +58 -0
  57. vtx/prompts/identity.py +166 -0
  58. vtx/prompts/tooling.py +36 -0
  59. vtx/py.typed +0 -0
  60. vtx/runtime.py +580 -0
  61. vtx/session.py +868 -0
  62. vtx/sounds/completion.wav +0 -0
  63. vtx/sounds/error.wav +0 -0
  64. vtx/sounds/permission.wav +0 -0
  65. vtx/themes.py +1104 -0
  66. vtx/tools/__init__.py +68 -0
  67. vtx/tools/_read_image.py +106 -0
  68. vtx/tools/_tool_utils.py +90 -0
  69. vtx/tools/base.py +36 -0
  70. vtx/tools/bash.py +371 -0
  71. vtx/tools/edit.py +261 -0
  72. vtx/tools/find.py +132 -0
  73. vtx/tools/read.py +238 -0
  74. vtx/tools/skill.py +278 -0
  75. vtx/tools/web.py +238 -0
  76. vtx/tools/write.py +88 -0
  77. vtx/tools_manager.py +216 -0
  78. vtx/turn.py +789 -0
  79. vtx/ui/__init__.py +0 -0
  80. vtx/ui/agent_runner.py +417 -0
  81. vtx/ui/app.py +665 -0
  82. vtx/ui/app_protocol.py +29 -0
  83. vtx/ui/autocomplete.py +440 -0
  84. vtx/ui/blocks.py +735 -0
  85. vtx/ui/chat.py +613 -0
  86. vtx/ui/clipboard.py +59 -0
  87. vtx/ui/commands/__init__.py +100 -0
  88. vtx/ui/commands/auth.py +306 -0
  89. vtx/ui/commands/base.py +122 -0
  90. vtx/ui/commands/models.py +144 -0
  91. vtx/ui/commands/sessions.py +388 -0
  92. vtx/ui/commands/settings.py +286 -0
  93. vtx/ui/completion_ui.py +313 -0
  94. vtx/ui/export.py +703 -0
  95. vtx/ui/floating_list.py +370 -0
  96. vtx/ui/formatting.py +287 -0
  97. vtx/ui/input.py +760 -0
  98. vtx/ui/latex.py +349 -0
  99. vtx/ui/launch.py +108 -0
  100. vtx/ui/path_complete.py +228 -0
  101. vtx/ui/prompt_history.py +102 -0
  102. vtx/ui/queue_ui.py +141 -0
  103. vtx/ui/selection_mode.py +18 -0
  104. vtx/ui/session_ui.py +235 -0
  105. vtx/ui/startup.py +124 -0
  106. vtx/ui/styles.py +327 -0
  107. vtx/ui/tool_output.py +34 -0
  108. vtx/ui/tree.py +437 -0
  109. vtx/ui/welcome.py +51 -0
  110. vtx/ui/widgets.py +558 -0
  111. vtx/update_check.py +49 -0
  112. vtx/version.py +22 -0
  113. vtx_coding_agent-0.1.1.dist-info/METADATA +259 -0
  114. vtx_coding_agent-0.1.1.dist-info/RECORD +117 -0
  115. vtx_coding_agent-0.1.1.dist-info/WHEEL +4 -0
  116. vtx_coding_agent-0.1.1.dist-info/entry_points.txt +2 -0
  117. 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))