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.
Files changed (283) hide show
  1. tau/__init__.py +0 -0
  2. tau/agent/__init__.py +11 -0
  3. tau/agent/prompt/__init__.py +10 -0
  4. tau/agent/prompt/builder.py +302 -0
  5. tau/agent/prompt/types.py +33 -0
  6. tau/agent/service.py +369 -0
  7. tau/agent/types.py +61 -0
  8. tau/auth/manager.py +247 -0
  9. tau/auth/storage.py +82 -0
  10. tau/auth/types.py +41 -0
  11. tau/builtins/__init__.py +4 -0
  12. tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
  13. tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
  14. tau/builtins/commands/__init__.py +41 -0
  15. tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  16. tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  17. tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
  18. tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
  19. tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
  20. tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
  21. tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
  22. tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
  23. tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
  24. tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
  25. tau/builtins/commands/clear.py +16 -0
  26. tau/builtins/commands/compact.py +28 -0
  27. tau/builtins/commands/reload.py +27 -0
  28. tau/builtins/commands/session.py +19 -0
  29. tau/builtins/extensions/footer/__init__.py +76 -0
  30. tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
  31. tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
  32. tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
  33. tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
  34. tau/builtins/extensions/footer/git.py +26 -0
  35. tau/builtins/extensions/footer/model.py +69 -0
  36. tau/builtins/extensions/footer/utils.py +44 -0
  37. tau/builtins/extensions/header/__init__.py +18 -0
  38. tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
  39. tau/builtins/models/__init__.py +0 -0
  40. tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
  41. tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
  42. tau/builtins/models/audio.py +43 -0
  43. tau/builtins/models/image.py +43 -0
  44. tau/builtins/models/text.py +482 -0
  45. tau/builtins/models/video.py +40 -0
  46. tau/builtins/prompts/commit.md +7 -0
  47. tau/builtins/prompts/docs.md +7 -0
  48. tau/builtins/prompts/explain.md +7 -0
  49. tau/builtins/prompts/fix.md +7 -0
  50. tau/builtins/prompts/refactor.md +7 -0
  51. tau/builtins/prompts/review.md +7 -0
  52. tau/builtins/prompts/test.md +7 -0
  53. tau/builtins/providers/__init__.py +0 -0
  54. tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  55. tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
  56. tau/builtins/providers/audio.py +10 -0
  57. tau/builtins/providers/image.py +9 -0
  58. tau/builtins/providers/text.py +33 -0
  59. tau/builtins/providers/video.py +6 -0
  60. tau/builtins/skills/code-review/SKILL.md +4 -0
  61. tau/builtins/skills/debug/SKILL.md +4 -0
  62. tau/builtins/skills/git-commit/SKILL.md +4 -0
  63. tau/builtins/themes/dark.yaml +1 -0
  64. tau/builtins/themes/light.yaml +46 -0
  65. tau/builtins/tools/__init__.py +73 -0
  66. tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  67. tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  68. tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
  69. tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
  70. tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
  71. tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
  72. tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
  73. tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
  74. tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
  75. tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
  76. tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
  77. tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
  78. tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
  79. tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
  80. tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
  81. tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
  82. tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
  83. tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
  84. tau/builtins/tools/edit.py +215 -0
  85. tau/builtins/tools/glob.py +112 -0
  86. tau/builtins/tools/grep.py +146 -0
  87. tau/builtins/tools/ls.py +135 -0
  88. tau/builtins/tools/read.py +122 -0
  89. tau/builtins/tools/terminal.py +150 -0
  90. tau/builtins/tools/write.py +105 -0
  91. tau/commands/__init__.py +10 -0
  92. tau/commands/registry.py +71 -0
  93. tau/commands/types.py +33 -0
  94. tau/console/__init__.py +0 -0
  95. tau/console/cli.py +266 -0
  96. tau/console/commands/__init__.py +0 -0
  97. tau/console/commands/auth.py +193 -0
  98. tau/console/commands/packages.py +104 -0
  99. tau/console/commands/update.py +76 -0
  100. tau/core/__init__.py +0 -0
  101. tau/core/registry.py +102 -0
  102. tau/engine/__init__.py +47 -0
  103. tau/engine/service.py +768 -0
  104. tau/engine/types.py +163 -0
  105. tau/extensions/__init__.py +28 -0
  106. tau/extensions/api.py +928 -0
  107. tau/extensions/context.py +462 -0
  108. tau/extensions/events.py +70 -0
  109. tau/extensions/loader.py +386 -0
  110. tau/extensions/runtime.py +184 -0
  111. tau/extensions/settings.py +137 -0
  112. tau/hooks/__init__.py +112 -0
  113. tau/hooks/engine.py +237 -0
  114. tau/hooks/inference.py +21 -0
  115. tau/hooks/runtime.py +126 -0
  116. tau/hooks/service.py +121 -0
  117. tau/hooks/session.py +117 -0
  118. tau/hooks/tui.py +61 -0
  119. tau/hooks/types.py +72 -0
  120. tau/inference/__init__.py +180 -0
  121. tau/inference/api/__init__.py +0 -0
  122. tau/inference/api/audio/__init__.py +0 -0
  123. tau/inference/api/audio/base.py +29 -0
  124. tau/inference/api/audio/builtins.py +15 -0
  125. tau/inference/api/audio/elevenlabs_audio.py +183 -0
  126. tau/inference/api/audio/gemini_audio.py +95 -0
  127. tau/inference/api/audio/openai_audio.py +159 -0
  128. tau/inference/api/audio/registry.py +15 -0
  129. tau/inference/api/audio/sarvam_audio.py +163 -0
  130. tau/inference/api/audio/service.py +103 -0
  131. tau/inference/api/audio/utils.py +47 -0
  132. tau/inference/api/image/__init__.py +0 -0
  133. tau/inference/api/image/base.py +17 -0
  134. tau/inference/api/image/builtins.py +8 -0
  135. tau/inference/api/image/gemini_image.py +77 -0
  136. tau/inference/api/image/openai_image.py +103 -0
  137. tau/inference/api/image/openrouter.py +144 -0
  138. tau/inference/api/image/registry.py +15 -0
  139. tau/inference/api/image/service.py +71 -0
  140. tau/inference/api/registry.py +82 -0
  141. tau/inference/api/text/__init__.py +0 -0
  142. tau/inference/api/text/anthropic_claude_code.py +222 -0
  143. tau/inference/api/text/anthropic_messages.py +196 -0
  144. tau/inference/api/text/base.py +40 -0
  145. tau/inference/api/text/builtins.py +19 -0
  146. tau/inference/api/text/gemini_generate.py +234 -0
  147. tau/inference/api/text/github_copilot_chat.py +172 -0
  148. tau/inference/api/text/google_antigravity.py +522 -0
  149. tau/inference/api/text/mistral_chat.py +284 -0
  150. tau/inference/api/text/ollama_chat.py +200 -0
  151. tau/inference/api/text/openai_codex_responses.py +497 -0
  152. tau/inference/api/text/openai_completions.py +227 -0
  153. tau/inference/api/text/openai_responses.py +235 -0
  154. tau/inference/api/text/registry.py +50 -0
  155. tau/inference/api/text/service.py +297 -0
  156. tau/inference/api/text/types.py +7 -0
  157. tau/inference/api/text/utils.py +228 -0
  158. tau/inference/api/video/__init__.py +0 -0
  159. tau/inference/api/video/base.py +26 -0
  160. tau/inference/api/video/builtins.py +7 -0
  161. tau/inference/api/video/fal_video.py +119 -0
  162. tau/inference/api/video/openrouter_video.py +142 -0
  163. tau/inference/api/video/registry.py +15 -0
  164. tau/inference/api/video/service.py +72 -0
  165. tau/inference/model/__init__.py +0 -0
  166. tau/inference/model/registry.py +102 -0
  167. tau/inference/model/types.py +65 -0
  168. tau/inference/provider/__init__.py +0 -0
  169. tau/inference/provider/oauth/__init__.py +35 -0
  170. tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
  171. tau/inference/provider/oauth/github_copilot.py +333 -0
  172. tau/inference/provider/oauth/google_antigravity.py +258 -0
  173. tau/inference/provider/oauth/openai_codex.py +309 -0
  174. tau/inference/provider/oauth/pkce.py +14 -0
  175. tau/inference/provider/oauth/types.py +46 -0
  176. tau/inference/provider/oauth/utils.py +154 -0
  177. tau/inference/provider/registry.py +141 -0
  178. tau/inference/provider/types.py +114 -0
  179. tau/inference/types.py +549 -0
  180. tau/inference/utils.py +219 -0
  181. tau/message/__init__.py +0 -0
  182. tau/message/types.py +482 -0
  183. tau/message/utils.py +178 -0
  184. tau/packages/__init__.py +11 -0
  185. tau/packages/manager.py +190 -0
  186. tau/packages/types.py +20 -0
  187. tau/packages/utils.py +67 -0
  188. tau/prompts/expand.py +58 -0
  189. tau/prompts/loader.py +69 -0
  190. tau/prompts/registry.py +45 -0
  191. tau/prompts/types.py +24 -0
  192. tau/rpc/__init__.py +8 -0
  193. tau/rpc/mode.py +783 -0
  194. tau/rpc/types.py +252 -0
  195. tau/runtime/service.py +759 -0
  196. tau/runtime/types.py +303 -0
  197. tau/session/branch_summarization.py +312 -0
  198. tau/session/compaction.py +646 -0
  199. tau/session/manager.py +652 -0
  200. tau/session/types.py +188 -0
  201. tau/session/utils.py +233 -0
  202. tau/settings/manager.py +1077 -0
  203. tau/settings/paths.py +150 -0
  204. tau/settings/storage.py +63 -0
  205. tau/settings/types.py +173 -0
  206. tau/settings/utils.py +25 -0
  207. tau/skills/loader.py +91 -0
  208. tau/skills/registry.py +70 -0
  209. tau/skills/types.py +25 -0
  210. tau/themes/loader.py +238 -0
  211. tau/themes/registry.py +108 -0
  212. tau/themes/types.py +19 -0
  213. tau/tool/__init__.py +3 -0
  214. tau/tool/registry.py +117 -0
  215. tau/tool/render.py +21 -0
  216. tau/tool/types.py +244 -0
  217. tau/trust/__init__.py +13 -0
  218. tau/trust/manager.py +80 -0
  219. tau/trust/types.py +14 -0
  220. tau/trust/utils.py +72 -0
  221. tau/tui/__init__.py +54 -0
  222. tau/tui/agent_hooks.py +346 -0
  223. tau/tui/ansi.py +330 -0
  224. tau/tui/app.py +540 -0
  225. tau/tui/autocomplete.py +33 -0
  226. tau/tui/capabilities.py +119 -0
  227. tau/tui/commands/__init__.py +3 -0
  228. tau/tui/commands/appearance.py +498 -0
  229. tau/tui/commands/auth.py +232 -0
  230. tau/tui/commands/context.py +38 -0
  231. tau/tui/commands/misc.py +82 -0
  232. tau/tui/commands/model.py +118 -0
  233. tau/tui/commands/session.py +464 -0
  234. tau/tui/component.py +268 -0
  235. tau/tui/components/__init__.py +0 -0
  236. tau/tui/components/autocomplete_manager.py +267 -0
  237. tau/tui/components/autocomplete_picker.py +143 -0
  238. tau/tui/components/box.py +90 -0
  239. tau/tui/components/command_palette.py +144 -0
  240. tau/tui/components/dynamic_border.py +19 -0
  241. tau/tui/components/file_picker.py +233 -0
  242. tau/tui/components/image.py +181 -0
  243. tau/tui/components/inline_selector.py +71 -0
  244. tau/tui/components/layout.py +1194 -0
  245. tau/tui/components/message_list.py +692 -0
  246. tau/tui/components/modal.py +97 -0
  247. tau/tui/components/model_palette.py +204 -0
  248. tau/tui/components/picker_overlay.py +174 -0
  249. tau/tui/components/prompt_overlay.py +236 -0
  250. tau/tui/components/resume_modal.py +372 -0
  251. tau/tui/components/select_list.py +222 -0
  252. tau/tui/components/settings_modal.py +274 -0
  253. tau/tui/components/settings_schema.py +203 -0
  254. tau/tui/components/spinner.py +119 -0
  255. tau/tui/components/text_input.py +396 -0
  256. tau/tui/components/text_prompt.py +82 -0
  257. tau/tui/components/tree_select_list.py +580 -0
  258. tau/tui/components/trust_screen.py +97 -0
  259. tau/tui/diff.py +114 -0
  260. tau/tui/fuzzy.py +99 -0
  261. tau/tui/input.py +496 -0
  262. tau/tui/input_handler.py +716 -0
  263. tau/tui/keybindings.py +87 -0
  264. tau/tui/markdown.py +286 -0
  265. tau/tui/message_renderers.py +31 -0
  266. tau/tui/overlay.py +326 -0
  267. tau/tui/renderer.py +378 -0
  268. tau/tui/terminal.py +499 -0
  269. tau/tui/theme.py +148 -0
  270. tau/tui/tui.py +544 -0
  271. tau/tui/ui_context.py +768 -0
  272. tau/tui/utils.py +20 -0
  273. tau/utils/__init__.py +0 -0
  274. tau/utils/http_proxy.py +221 -0
  275. tau/utils/image_processing.py +172 -0
  276. tau/utils/secrets.py +59 -0
  277. tau/utils/version_check.py +60 -0
  278. tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
  279. tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
  280. tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
  281. tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  282. tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  283. tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
@@ -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
@@ -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)
tau/rpc/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from tau.rpc.types import RpcResponse, RpcSessionState
2
+ from tau.rpc.mode import run_rpc_mode
3
+
4
+ __all__ = [
5
+ "run_rpc_mode",
6
+ "RpcResponse",
7
+ "RpcSessionState",
8
+ ]