tau-coding-agent 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.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
tau/packages/manager.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from tau.packages.types import ParsedSource
|
|
10
|
+
from tau.packages.utils import parse_source, extensions_from_pyproject
|
|
11
|
+
from tau.settings.paths import get_app_name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PackageManager:
|
|
15
|
+
"""Manages Python extension packages in a dedicated venv."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, venv_dir: Path) -> None:
|
|
18
|
+
self.venv_dir = venv_dir
|
|
19
|
+
|
|
20
|
+
# ── Venv paths ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def _python(self) -> Path:
|
|
24
|
+
"""Return the path to the venv's Python executable."""
|
|
25
|
+
if sys.platform == "win32":
|
|
26
|
+
return self.venv_dir / "Scripts" / "python.exe"
|
|
27
|
+
return self.venv_dir / "bin" / "python"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def _pip_exe(self) -> Path:
|
|
31
|
+
"""Return the path to the venv's pip executable."""
|
|
32
|
+
if sys.platform == "win32":
|
|
33
|
+
return self.venv_dir / "Scripts" / "pip.exe"
|
|
34
|
+
return self.venv_dir / "bin" / "pip"
|
|
35
|
+
|
|
36
|
+
def _has_uv(self) -> bool:
|
|
37
|
+
"""Check if uv package manager is installed."""
|
|
38
|
+
return shutil.which("uv") is not None
|
|
39
|
+
|
|
40
|
+
def ensure_venv(self) -> None:
|
|
41
|
+
"""Create the venv if it does not already exist."""
|
|
42
|
+
if self._python.exists():
|
|
43
|
+
return
|
|
44
|
+
self.venv_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
if self._has_uv():
|
|
46
|
+
subprocess.run(["uv", "venv", str(self.venv_dir)], check=True, capture_output=True)
|
|
47
|
+
else:
|
|
48
|
+
subprocess.run(
|
|
49
|
+
[sys.executable, "-m", "venv", str(self.venv_dir)],
|
|
50
|
+
check=True, capture_output=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def site_packages(self) -> Path | None:
|
|
54
|
+
"""Return the venv's site-packages directory."""
|
|
55
|
+
if not self._python.exists():
|
|
56
|
+
return None
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
[str(self._python), "-c", "import site; print(site.getsitepackages()[0])"],
|
|
59
|
+
capture_output=True, text=True,
|
|
60
|
+
)
|
|
61
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
62
|
+
return Path(result.stdout.strip())
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# ── Package operations ────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def install(self, source: str) -> "PackageEntry":
|
|
68
|
+
"""Install a package and return a PackageEntry with metadata."""
|
|
69
|
+
from tau.settings.types import PackageEntry
|
|
70
|
+
parsed = parse_source(source)
|
|
71
|
+
self.ensure_venv()
|
|
72
|
+
|
|
73
|
+
if self._has_uv():
|
|
74
|
+
cmd = ["uv", "pip", "install", "--python", str(self._python), parsed.install_spec]
|
|
75
|
+
else:
|
|
76
|
+
cmd = [str(self._pip_exe), "install", parsed.install_spec]
|
|
77
|
+
subprocess.run(cmd, check=True)
|
|
78
|
+
|
|
79
|
+
installed_path = self._find_package_dir(parsed.name)
|
|
80
|
+
version = parsed.version or self._get_installed_version(parsed.name)
|
|
81
|
+
|
|
82
|
+
return PackageEntry(
|
|
83
|
+
source=source,
|
|
84
|
+
name=parsed.name,
|
|
85
|
+
version=version,
|
|
86
|
+
installed_path=str(installed_path) if installed_path else None,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def remove(self, name: str) -> None:
|
|
90
|
+
"""Uninstall a package from the venv."""
|
|
91
|
+
if self._has_uv():
|
|
92
|
+
cmd = ["uv", "pip", "uninstall", "--python", str(self._python), name]
|
|
93
|
+
else:
|
|
94
|
+
cmd = [str(self._pip_exe), "uninstall", "-y", name]
|
|
95
|
+
subprocess.run(cmd, check=True)
|
|
96
|
+
|
|
97
|
+
def install_requirements(self, dependencies: list[str]) -> None:
|
|
98
|
+
"""Install a batch of dependency specs (e.g. extension-declared requirements)."""
|
|
99
|
+
if not dependencies:
|
|
100
|
+
return
|
|
101
|
+
self.ensure_venv()
|
|
102
|
+
if self._has_uv():
|
|
103
|
+
cmd = ["uv", "pip", "install", "--python", str(self._python), *dependencies]
|
|
104
|
+
else:
|
|
105
|
+
cmd = [str(self._pip_exe), "install", *dependencies]
|
|
106
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
|
107
|
+
|
|
108
|
+
def update(self, name: str) -> str | None:
|
|
109
|
+
"""Upgrade a package to the latest version and return the new version string."""
|
|
110
|
+
if self._has_uv():
|
|
111
|
+
cmd = ["uv", "pip", "install", "--python", str(self._python), "--upgrade", name]
|
|
112
|
+
else:
|
|
113
|
+
cmd = [str(self._pip_exe), "install", "--upgrade", name]
|
|
114
|
+
subprocess.run(cmd, check=True)
|
|
115
|
+
return self._get_installed_version(name)
|
|
116
|
+
|
|
117
|
+
# ── Extension discovery ───────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
def find_extension_files(self, name: str, installed_path: str | None = None) -> list[Path]:
|
|
120
|
+
"""Return the extension .py files for an installed package.
|
|
121
|
+
|
|
122
|
+
Discovery order:
|
|
123
|
+
1. manifest.json with {get_app_name_lower(): {"extensions": [...]}}
|
|
124
|
+
2. pyproject.toml with [tool.{get_app_name_lower()}] extensions list
|
|
125
|
+
3. __init__.py that defines register()
|
|
126
|
+
"""
|
|
127
|
+
if installed_path:
|
|
128
|
+
pkg_dir = Path(installed_path)
|
|
129
|
+
else:
|
|
130
|
+
pkg_dir = self._find_package_dir(name)
|
|
131
|
+
|
|
132
|
+
if not pkg_dir or not pkg_dir.is_dir():
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
# 1. manifest.json
|
|
136
|
+
manifest = pkg_dir / "manifest.json"
|
|
137
|
+
if manifest.is_file():
|
|
138
|
+
try:
|
|
139
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
140
|
+
declared = data.get(get_app_name().lower(), {}).get("extensions", [])
|
|
141
|
+
if declared:
|
|
142
|
+
return [(pkg_dir / p).resolve() for p in declared if (pkg_dir / p).is_file()]
|
|
143
|
+
except (json.JSONDecodeError, OSError):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# 2. pyproject.toml (package dir or its parent)
|
|
147
|
+
for pp in [pkg_dir / "pyproject.toml", pkg_dir.parent / "pyproject.toml"]:
|
|
148
|
+
if pp.is_file():
|
|
149
|
+
found = extensions_from_pyproject(pp, pp.parent)
|
|
150
|
+
if found:
|
|
151
|
+
return found
|
|
152
|
+
|
|
153
|
+
# 3. __init__.py with a register() function
|
|
154
|
+
init = pkg_dir / "__init__.py"
|
|
155
|
+
if init.is_file():
|
|
156
|
+
try:
|
|
157
|
+
content = init.read_text(encoding="utf-8")
|
|
158
|
+
if "def register(" in content or "async def register(" in content:
|
|
159
|
+
return [init.resolve()]
|
|
160
|
+
except OSError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def _get_installed_version(self, name: str) -> str | None:
|
|
168
|
+
"""Query the installed version of a package."""
|
|
169
|
+
if not self._python.exists():
|
|
170
|
+
return None
|
|
171
|
+
for n in [name.replace("-", "_").lower(), name.lower()]:
|
|
172
|
+
result = subprocess.run(
|
|
173
|
+
[str(self._python), "-c",
|
|
174
|
+
f"import importlib.metadata; print(importlib.metadata.version({n!r}))"],
|
|
175
|
+
capture_output=True, text=True,
|
|
176
|
+
)
|
|
177
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
178
|
+
return result.stdout.strip()
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _find_package_dir(self, name: str) -> Path | None:
|
|
182
|
+
"""Locate the installation directory of a package in site-packages."""
|
|
183
|
+
site_pkgs = self.site_packages()
|
|
184
|
+
if not site_pkgs or not site_pkgs.is_dir():
|
|
185
|
+
return None
|
|
186
|
+
for candidate in [name, name.replace("-", "_"), name.replace("-", "_").lower()]:
|
|
187
|
+
p = site_pkgs / candidate
|
|
188
|
+
if p.is_dir():
|
|
189
|
+
return p
|
|
190
|
+
return None
|
tau/packages/types.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SourceType(str, Enum):
|
|
9
|
+
PYPI = "pypi"
|
|
10
|
+
GIT = "git"
|
|
11
|
+
LOCAL = "local"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ParsedSource:
|
|
16
|
+
source: SourceType
|
|
17
|
+
raw: str
|
|
18
|
+
name: str
|
|
19
|
+
version: Optional[str] = None
|
|
20
|
+
install_spec: Optional[str] = None # argument passed to pip install
|
tau/packages/utils.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from tau.packages.types import ParsedSource, SourceType
|
|
7
|
+
from tau.settings.paths import get_app_name
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_source(source: str) -> ParsedSource:
|
|
11
|
+
"""Parse a package source string into its components.
|
|
12
|
+
|
|
13
|
+
Supported formats:
|
|
14
|
+
pypi:package-name
|
|
15
|
+
pypi:package-name@1.0.0
|
|
16
|
+
git+https://github.com/user/repo
|
|
17
|
+
git+https://github.com/user/repo@v1
|
|
18
|
+
/absolute/path or ./relative/path or ~/path
|
|
19
|
+
bare-name (treated as pypi)
|
|
20
|
+
"""
|
|
21
|
+
s = source.strip()
|
|
22
|
+
|
|
23
|
+
if s.startswith("pypi:"):
|
|
24
|
+
rest = s[5:]
|
|
25
|
+
if "@" in rest:
|
|
26
|
+
name, _, version = rest.partition("@")
|
|
27
|
+
else:
|
|
28
|
+
name, version = rest, None
|
|
29
|
+
name = name.strip()
|
|
30
|
+
spec = f"{name}=={version}" if version else name
|
|
31
|
+
return ParsedSource(source=SourceType.PYPI, raw=source, name=name, version=version, install_spec=spec)
|
|
32
|
+
|
|
33
|
+
if s.startswith("git+"):
|
|
34
|
+
# git+https://github.com/user/repo@tag → name = "repo"
|
|
35
|
+
base = re.sub(r"@[^/]+$", "", s) if "@" in s else s
|
|
36
|
+
name = re.sub(r"\.git$", "", base).rstrip("/").split("/")[-1]
|
|
37
|
+
return ParsedSource(source=SourceType.GIT, raw=source, name=name, install_spec=source)
|
|
38
|
+
|
|
39
|
+
if s.startswith(("/", ".", "~")):
|
|
40
|
+
path = Path(s).expanduser().resolve()
|
|
41
|
+
return ParsedSource(source=SourceType.LOCAL, raw=source, name=path.name, install_spec=str(path))
|
|
42
|
+
|
|
43
|
+
# Bare name — treat as pypi
|
|
44
|
+
m = re.match(r"^([a-zA-Z0-9_.-]+)(?:@(.+))?$", s)
|
|
45
|
+
if m:
|
|
46
|
+
name, version = m.group(1), m.group(2)
|
|
47
|
+
spec = f"{name}=={version}" if version else name
|
|
48
|
+
return ParsedSource(source=SourceType.PYPI, raw=source, name=name, version=version, install_spec=spec)
|
|
49
|
+
|
|
50
|
+
raise ValueError(f"Cannot parse package source: {source!r}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extensions_from_pyproject(pyproject: Path, base: Path) -> list[Path]:
|
|
54
|
+
"""Read [tool.tau].extensions from a pyproject.toml and return resolved paths."""
|
|
55
|
+
try:
|
|
56
|
+
try:
|
|
57
|
+
import tomllib # Python 3.11+
|
|
58
|
+
except ImportError:
|
|
59
|
+
try:
|
|
60
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
61
|
+
except ImportError:
|
|
62
|
+
return []
|
|
63
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
64
|
+
declared = data.get("tool", {}).get(get_app_name().lower(), {}).get("extensions", [])
|
|
65
|
+
return [(base / p).resolve() for p in declared if (base / p).is_file()]
|
|
66
|
+
except Exception:
|
|
67
|
+
return []
|
tau/prompts/expand.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _parse_args(args_str: str) -> list[str]:
|
|
8
|
+
"""Parse a shell-style argument string into a list of arguments."""
|
|
9
|
+
try:
|
|
10
|
+
return shlex.split(args_str)
|
|
11
|
+
except ValueError:
|
|
12
|
+
return args_str.split()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def expand(content: str, args_str: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Substitute argument placeholders in template content.
|
|
18
|
+
|
|
19
|
+
Patterns supported:
|
|
20
|
+
$1, $2, ... Positional argument (1-based)
|
|
21
|
+
$@ or $ARGUMENTS All arguments joined with spaces
|
|
22
|
+
${1:-default} Positional with fallback default
|
|
23
|
+
${@:N} Args from index N (1-based) joined
|
|
24
|
+
${@:N:L} Args from N, length L
|
|
25
|
+
"""
|
|
26
|
+
args = _parse_args(args_str.strip()) if args_str.strip() else []
|
|
27
|
+
all_args = " ".join(args)
|
|
28
|
+
|
|
29
|
+
def _brace(match: re.Match) -> str:
|
|
30
|
+
inner = match.group(1)
|
|
31
|
+
|
|
32
|
+
# ${@:N} or ${@:N:L}
|
|
33
|
+
m = re.match(r"@:(\d+)(?::(\d+))?$", inner)
|
|
34
|
+
if m:
|
|
35
|
+
n = int(m.group(1)) - 1
|
|
36
|
+
length = int(m.group(2)) if m.group(2) else None
|
|
37
|
+
sliced = args[n : n + length] if length is not None else args[n:]
|
|
38
|
+
return " ".join(sliced)
|
|
39
|
+
|
|
40
|
+
# ${N:-default}
|
|
41
|
+
m = re.match(r"(\d+):-(.*)$", inner)
|
|
42
|
+
if m:
|
|
43
|
+
idx = int(m.group(1)) - 1
|
|
44
|
+
return args[idx] if idx < len(args) else m.group(2)
|
|
45
|
+
|
|
46
|
+
# ${N}
|
|
47
|
+
m = re.match(r"(\d+)$", inner)
|
|
48
|
+
if m:
|
|
49
|
+
idx = int(m.group(1)) - 1
|
|
50
|
+
return args[idx] if idx < len(args) else ""
|
|
51
|
+
|
|
52
|
+
return match.group(0)
|
|
53
|
+
|
|
54
|
+
result = re.sub(r"\$\{([^}]+)\}", _brace, content)
|
|
55
|
+
result = re.sub(r"\$(?:@|ARGUMENTS)\b", all_args, result)
|
|
56
|
+
result = re.sub(r"\$([1-9])\b", lambda m: args[int(m.group(1)) - 1] if int(m.group(1)) - 1 < len(args) else "", result)
|
|
57
|
+
|
|
58
|
+
return result
|
tau/prompts/loader.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from tau.prompts.types import LoadPromptsResult, PromptLoadError, PromptTemplate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
|
|
9
|
+
"""Parse YAML frontmatter from markdown text."""
|
|
10
|
+
text = text.lstrip("\n")
|
|
11
|
+
if not text.startswith("---"):
|
|
12
|
+
return {}, text
|
|
13
|
+
end = text.find("\n---", 3)
|
|
14
|
+
if end == -1:
|
|
15
|
+
return {}, text
|
|
16
|
+
fm_text = text[3:end].strip()
|
|
17
|
+
body = text[end + 4:].lstrip("\n")
|
|
18
|
+
meta: dict[str, str] = {}
|
|
19
|
+
for line in fm_text.splitlines():
|
|
20
|
+
if ":" in line:
|
|
21
|
+
key, _, val = line.partition(":")
|
|
22
|
+
meta[key.strip().lower()] = val.strip()
|
|
23
|
+
return meta, body
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_template_from_file(path: Path) -> tuple[PromptTemplate | None, str | None]:
|
|
27
|
+
"""Load a prompt template from a markdown file."""
|
|
28
|
+
try:
|
|
29
|
+
text = path.read_text(encoding="utf-8")
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
return None, f"read error: {exc}"
|
|
32
|
+
|
|
33
|
+
meta, body = _parse_frontmatter(text)
|
|
34
|
+
body = body.strip()
|
|
35
|
+
if not body:
|
|
36
|
+
return None, "template body is empty"
|
|
37
|
+
|
|
38
|
+
name = path.stem.lower()
|
|
39
|
+
description = meta.get("description", "")
|
|
40
|
+
if not description:
|
|
41
|
+
for line in body.splitlines():
|
|
42
|
+
stripped = line.strip().lstrip("#").strip()
|
|
43
|
+
if stripped:
|
|
44
|
+
description = stripped[:120]
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
argument_hint = meta.get("argument-hint") or meta.get("argument_hint") or None
|
|
48
|
+
|
|
49
|
+
return PromptTemplate(
|
|
50
|
+
name=name,
|
|
51
|
+
description=description,
|
|
52
|
+
content=body,
|
|
53
|
+
argument_hint=argument_hint,
|
|
54
|
+
file_path=str(path),
|
|
55
|
+
), None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_templates_from_dir(directory: Path) -> LoadPromptsResult:
|
|
59
|
+
"""Load all prompt templates from a directory."""
|
|
60
|
+
result = LoadPromptsResult()
|
|
61
|
+
if not directory.is_dir():
|
|
62
|
+
return result
|
|
63
|
+
for path in sorted(directory.glob("*.md")):
|
|
64
|
+
tmpl, err = load_template_from_file(path)
|
|
65
|
+
if err or tmpl is None:
|
|
66
|
+
result.errors.append(PromptLoadError(str(path), err or "unknown error"))
|
|
67
|
+
continue
|
|
68
|
+
result.templates[tmpl.name] = tmpl
|
|
69
|
+
return result
|
tau/prompts/registry.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt template registry.
|
|
3
|
+
|
|
4
|
+
Priority (highest wins):
|
|
5
|
+
project (.tau/prompts/*.md relative to cwd)
|
|
6
|
+
global (~/.tau/prompts/*.md)
|
|
7
|
+
builtin (tau/builtins/prompts/)
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from tau.core.registry import Registry
|
|
15
|
+
from tau.prompts.types import PromptLoadError, PromptTemplate
|
|
16
|
+
from tau.prompts.loader import load_templates_from_dir
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PromptRegistry(Registry[PromptTemplate, PromptLoadError]):
|
|
20
|
+
def _load_from_dir(self, path: Path) -> Any:
|
|
21
|
+
return load_templates_from_dir(path)
|
|
22
|
+
|
|
23
|
+
def _get_dir(self, cwd: Path | None = None) -> Path:
|
|
24
|
+
from tau.settings.paths import get_prompts_dir
|
|
25
|
+
return get_prompts_dir(cwd)
|
|
26
|
+
|
|
27
|
+
def _builtins_subdir(self) -> str:
|
|
28
|
+
return "prompts"
|
|
29
|
+
|
|
30
|
+
def _extract_items(self, result: Any) -> dict[str, PromptTemplate]:
|
|
31
|
+
return result.templates
|
|
32
|
+
|
|
33
|
+
def _extract_errors(self, result: Any) -> list[PromptLoadError]:
|
|
34
|
+
return result.errors
|
|
35
|
+
|
|
36
|
+
def expand(self, name: str, args_str: str) -> str | None:
|
|
37
|
+
"""Expand a prompt template with the given arguments."""
|
|
38
|
+
tmpl = self.get(name)
|
|
39
|
+
if tmpl is None:
|
|
40
|
+
return None
|
|
41
|
+
from tau.prompts.expand import expand as _expand
|
|
42
|
+
return _expand(tmpl.content, args_str)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
prompt_registry = PromptRegistry()
|
tau/prompts/types.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class PromptTemplate:
|
|
8
|
+
name: str
|
|
9
|
+
description: str
|
|
10
|
+
content: str
|
|
11
|
+
argument_hint: str | None = None
|
|
12
|
+
file_path: str = ""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PromptLoadError:
|
|
17
|
+
path: str
|
|
18
|
+
error: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class LoadPromptsResult:
|
|
23
|
+
templates: dict[str, PromptTemplate] = field(default_factory=dict)
|
|
24
|
+
errors: list[PromptLoadError] = field(default_factory=list)
|