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,105 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from tau.tool.types import (
9
+ Tool, ToolKind, ToolExecutionMode,
10
+ ToolInvocation, ToolResult,
11
+ ToolExecutionUpdateCallback, AbortSignal, ToolContext,
12
+ )
13
+ from tau.tool.render import call_line
14
+
15
+
16
+ def _render_write_call(args: dict, _streaming: bool) -> list[str]:
17
+ return call_line("write", args.get("path", ""))
18
+
19
+
20
+ class WriteParams(BaseModel):
21
+ """Parameters for the write tool."""
22
+ path: str = Field(description="Absolute path to the file to write.")
23
+ content: str = Field(description="Content to write to the file.")
24
+
25
+
26
+ _PREVIEW_LINES = 5
27
+
28
+
29
+ def _render_write_result(content: str, opts: Any) -> list[str]:
30
+ from tau.tui.ansi import DIM, GREEN, RESET
31
+ metadata = opts.metadata or {}
32
+ total_lines = metadata.get("total_lines", 0)
33
+ created = metadata.get("created", False)
34
+ lines = metadata.get("lines", [])
35
+
36
+ action = f"{GREEN}Created{RESET}" if created else "Written"
37
+ line_word = "line" if total_lines == 1 else "lines"
38
+ result = [f"{action} {total_lines} {line_word}"]
39
+
40
+ if not lines:
41
+ return result
42
+
43
+ show = lines if opts.expanded else lines[:_PREVIEW_LINES]
44
+ for i, text in enumerate(show, 1):
45
+ result.append(f"{DIM}{i}{RESET} {text}")
46
+
47
+ if opts.expanded and len(lines) > _PREVIEW_LINES:
48
+ result.append(f"{DIM} (ctrl+o to collapse){RESET}")
49
+ elif not opts.expanded and len(lines) > _PREVIEW_LINES:
50
+ result.append(f"{DIM} ··· (ctrl+o to expand){RESET}")
51
+
52
+ return result
53
+
54
+
55
+ class WriteTool(Tool):
56
+ """Tool for writing content to files."""
57
+ def __init__(self) -> None:
58
+ super().__init__(
59
+ name="write",
60
+ description=(
61
+ "Write content to a file, creating it (and any missing parent directories) if needed. "
62
+ "Overwrites the file if it already exists."
63
+ ),
64
+ schema=WriteParams,
65
+ kind=ToolKind.Write,
66
+ render_result=_render_write_result,
67
+ render_call=_render_write_call,
68
+ render_shell="default",
69
+ prompt_guidelines="Only use for new files or complete rewrites. Use edit to modify existing files.",
70
+ )
71
+
72
+ def get_display_name(self, args: dict[str, Any]) -> str:
73
+ """Get a short display name for the write operation."""
74
+ return args.get("path", "write")
75
+
76
+ async def execute(
77
+ self,
78
+ invocation: ToolInvocation,
79
+ tool_execution_update_callback: Optional[ToolExecutionUpdateCallback] = None,
80
+ signal: Optional[AbortSignal] = None,
81
+ context: Optional[ToolContext] = None,
82
+ ) -> ToolResult:
83
+ """Execute the file write operation."""
84
+ params = WriteParams.model_validate(invocation.params)
85
+ path = Path(params.path)
86
+
87
+ created = not path.exists()
88
+
89
+ try:
90
+ path.parent.mkdir(parents=True, exist_ok=True)
91
+ path.write_text(params.content, encoding="utf-8")
92
+ except OSError as e:
93
+ return ToolResult.error(invocation.id, f"Cannot write file: {e}")
94
+
95
+ bytes_written = len(params.content.encode("utf-8"))
96
+ content_lines = params.content.splitlines()
97
+ total_lines = len(content_lines)
98
+ metadata = {
99
+ "file_path": str(path),
100
+ "total_lines": total_lines,
101
+ "bytes_written": bytes_written,
102
+ "created": created,
103
+ "lines": content_lines,
104
+ }
105
+ return ToolResult.ok(invocation.id, f"Written {bytes_written} bytes to {params.path}", metadata=metadata)
@@ -0,0 +1,10 @@
1
+ """Slash-command registry for the coding agent."""
2
+
3
+ from tau.commands.types import CommandInfo, ParsedCommand
4
+ from tau.commands.registry import CommandRegistry
5
+
6
+ __all__ = [
7
+ 'CommandInfo',
8
+ 'ParsedCommand',
9
+ 'CommandRegistry',
10
+ ]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING
5
+
6
+ from tau.commands.types import CommandInfo, ParsedCommand
7
+
8
+ if TYPE_CHECKING:
9
+ from tau.runtime.service import Runtime
10
+
11
+
12
+ class CommandRegistry:
13
+ """
14
+ Holds all registered slash commands and dispatches parsed input.
15
+ Attach a Runtime so handlers can call back into session lifecycle methods.
16
+ """
17
+
18
+ def __init__(self, runtime: Runtime | None = None) -> None:
19
+ """Initialize the command registry with optional runtime context."""
20
+ self.runtime = runtime
21
+ self._commands: dict[str, CommandInfo] = {}
22
+ from tau.builtins.commands import get_builtin_commands
23
+ for cmd in get_builtin_commands():
24
+ self.register(cmd)
25
+
26
+ def register(self, command: CommandInfo) -> None:
27
+ """Register a command with its name and aliases."""
28
+ self._commands[command.name] = command
29
+ for alias in command.aliases:
30
+ self._commands[alias] = command
31
+
32
+ def unregister(self, name: str) -> None:
33
+ """Remove a command and all its aliases."""
34
+ cmd = self._commands.get(name)
35
+ if cmd is None:
36
+ return
37
+ keys = [k for k, v in self._commands.items() if v is cmd]
38
+ for k in keys:
39
+ del self._commands[k]
40
+
41
+ def get(self, name: str) -> CommandInfo | None:
42
+ """Retrieve a command by name or alias."""
43
+ return self._commands.get(name)
44
+
45
+ def list(self) -> list[CommandInfo]:
46
+ """Return all registered commands (de-duplicated by name)."""
47
+ seen: set[str] = set()
48
+ result: list[CommandInfo] = []
49
+ for cmd in self._commands.values():
50
+ if cmd.name not in seen:
51
+ seen.add(cmd.name)
52
+ result.append(cmd)
53
+ return result
54
+
55
+ async def dispatch(self, parsed: ParsedCommand) -> bool:
56
+ """Invoke the matching command; return True if dispatched, False if not found."""
57
+ cmd = self._commands.get(parsed.name)
58
+ if cmd is None:
59
+ return False
60
+
61
+ missing = cmd.required_arg_names[len(parsed.args):]
62
+ if missing:
63
+ if self.runtime is not None:
64
+ plural = "s" if len(missing) > 1 else ""
65
+ self.runtime.notify(f"Missing required argument{plural}: {', '.join(missing)}")
66
+ return True
67
+
68
+ result = cmd.call(self, parsed.args)
69
+ if asyncio.iscoroutine(result):
70
+ await result
71
+ return True
tau/commands/types.py ADDED
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Awaitable, Callable
5
+
6
+ if TYPE_CHECKING:
7
+ from tau.commands.registry import CommandRegistry
8
+ from tau.tui.autocomplete import AutocompleteItem
9
+
10
+
11
+ @dataclass
12
+ class CommandInfo:
13
+ """Metadata for a registered command."""
14
+ name: str
15
+ description: str
16
+ call: Callable[['CommandRegistry', list[str]], Awaitable[None] | None]
17
+ aliases: list[str] = field(default_factory=list)
18
+ argument_hint: str | None = None
19
+ get_argument_completions: Callable[[str], list['AutocompleteItem']] | None = None
20
+ required_arg_names: list[str] = field(default_factory=list)
21
+ """Names of the leading positional args that must be present, in order.
22
+
23
+ Any args beyond these are treated as optional. Declare required args
24
+ before optional ones, since this only checks a minimum count.
25
+ """
26
+
27
+
28
+ @dataclass
29
+ class ParsedCommand:
30
+ """Result of parsing a command string into name, args, and raw input."""
31
+ name: str
32
+ args: list[str]
33
+ raw: str
File without changes
tau/console/cli.py ADDED
@@ -0,0 +1,266 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import os
5
+ import logging
6
+ import click
7
+ import asyncio
8
+ from pathlib import Path
9
+ from tau.runtime.service import Runtime
10
+ from tau.console.commands.auth import auth
11
+ from tau.console.commands.packages import install, remove, list_packages
12
+ from tau.console.commands.update import update
13
+ from tau.settings.paths import get_app_version
14
+
15
+ _MODES = ("interactive", "print", "json", "rpc")
16
+ _OUTPUT_FORMATS = ("text", "json")
17
+
18
+
19
+ def resolve_mode(mode: str | None, print_flag: bool, prompt: str | None, output_format: str) -> str:
20
+ """Determine the run mode: interactive, print, json, or rpc."""
21
+ if mode is not None:
22
+ return mode
23
+ if prompt is not None:
24
+ return "json" if output_format == "json" else "print"
25
+ if print_flag or not sys.stdout.isatty():
26
+ return "print"
27
+ return "interactive"
28
+
29
+
30
+ def resolve_model(model: str | None, provider: str | None) -> tuple[str | None, str | None]:
31
+ """Parse provider/model shorthand. Explicit --provider always wins."""
32
+ if model and provider is None and "/" in model:
33
+ inferred_provider, _, model_id = model.partition("/")
34
+ return inferred_provider, model_id
35
+ return provider, model # None when not specified; runtime falls back to settings then default
36
+
37
+
38
+ @click.group(invoke_without_command=True, context_settings={"help_option_names": ["-h", "--help"]})
39
+ @click.argument("message", required=False, default=None)
40
+ @click.option("--version", "-v", is_flag=True, default=False, help="Print version and exit.")
41
+ @click.option("--debug", "-d", is_flag=True, default=False, help="Enable debug logging.")
42
+ @click.option("--cwd", "-c", default=None, metavar="PATH", help="Set the working directory.")
43
+ @click.option("--prompt", "-p", default=None, metavar="TEXT",
44
+ help="Run a single prompt in non-interactive mode.")
45
+ @click.option("--output-format", "-f", type=click.Choice(_OUTPUT_FORMATS), default="text", show_default=True,
46
+ help="Output format for non-interactive mode (text, json).")
47
+ @click.option("--quiet", "-q", is_flag=True, default=False,
48
+ help="Hide spinner in non-interactive mode.")
49
+ @click.option("--provider", default=None, help="Provider to use (e.g. groq, mistral, openrouter).")
50
+ @click.option("--model", default=None, help="Model ID, or provider/model shorthand (e.g. groq/llama-3.3-70b-versatile).")
51
+ @click.option("--theme", "-t", default=None, metavar="NAME",
52
+ help="UI theme: default, dracula, nord, gruvbox, catppuccin.")
53
+ @click.option("--resume", "-r", is_flag=True, default=False,
54
+ help="Resume the most recent session.")
55
+ @click.option("--system", "-s", default=None, metavar="TEXT",
56
+ help="Inject additional text into the system prompt.")
57
+ @click.option("--session", default=None, metavar="ID",
58
+ help="Resume a specific session by ID or path.")
59
+ @click.option("--ephemeral", "-e", is_flag=True, default=False,
60
+ help="Don't save this session to disk.")
61
+ @click.option("--print", "print_flag", is_flag=True, default=False,
62
+ help="Shorthand for --mode print.")
63
+ @click.option("--mode", type=click.Choice(_MODES), default=None,
64
+ help="Run mode: interactive (default), print, json, rpc.")
65
+ @click.option("--no-context-files", "-nc", is_flag=True, default=False,
66
+ help="Disable AGENTS.md and CLAUDE.md discovery and loading.")
67
+ @click.option("--approve", "-a", is_flag=True, default=False,
68
+ help="Trust project-local files (extensions, settings, context files).")
69
+ @click.option("--no-approve", "-na", is_flag=True, default=False,
70
+ help="Don't trust project-local files (opposite of --approve).")
71
+ @click.pass_context
72
+ def cli(
73
+ ctx: click.Context,
74
+ message: str | None,
75
+ version: bool,
76
+ debug: bool,
77
+ cwd: str | None,
78
+ prompt: str | None,
79
+ output_format: str,
80
+ quiet: bool,
81
+ provider: str | None,
82
+ model: str | None,
83
+ theme: str | None,
84
+ resume: bool,
85
+ system: str | None,
86
+ session: str | None,
87
+ ephemeral: bool,
88
+ print_flag: bool,
89
+ mode: str | None,
90
+ no_context_files: bool,
91
+ approve: bool,
92
+ no_approve: bool,
93
+ ) -> None:
94
+ """Tau — an AI coding agent in your terminal."""
95
+ if version:
96
+ click.echo(get_app_version())
97
+ return
98
+
99
+ if debug:
100
+ logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(name)s: %(message)s")
101
+
102
+ if cwd:
103
+ os.chdir(cwd)
104
+
105
+ ctx.ensure_object(dict)
106
+ ctx.obj["message"] = prompt or message
107
+ ctx.obj["provider"] = provider
108
+ ctx.obj["model"] = model
109
+ ctx.obj["theme"] = theme
110
+ ctx.obj["resume"] = resume
111
+ ctx.obj["system"] = system or ""
112
+ ctx.obj["session"] = session
113
+ ctx.obj["ephemeral"] = ephemeral
114
+ ctx.obj["quiet"] = quiet
115
+ ctx.obj["mode"] = resolve_mode(mode, print_flag, prompt, output_format)
116
+ ctx.obj["no_context_files"] = no_context_files
117
+ ctx.obj["approve"] = approve
118
+ ctx.obj["no_approve"] = no_approve
119
+
120
+ if ctx.invoked_subcommand is None:
121
+ asyncio.run(_start(ctx.obj))
122
+
123
+
124
+ async def _start(opts: dict) -> None:
125
+ """Start the runtime with the given options and run in the specified mode."""
126
+ from tau.runtime.service import Runtime
127
+ from tau.runtime.types import RuntimeConfig
128
+
129
+ resolved_provider, resolved_model = resolve_model(opts["model"], opts["provider"])
130
+ session_file = Path(opts["session"]).resolve() if opts["session"] else None
131
+
132
+ # Determine project trust from flags
133
+ project_trusted = None
134
+ if opts.get("approve"):
135
+ project_trusted = True
136
+ elif opts.get("no_approve"):
137
+ project_trusted = False
138
+
139
+ config = RuntimeConfig(
140
+ cwd=Path.cwd(),
141
+ model_id=resolved_model,
142
+ provider=resolved_provider,
143
+ resume=opts["resume"],
144
+ session_file=session_file,
145
+ persist_session=not opts["ephemeral"],
146
+ mode=opts["mode"],
147
+ system_prompt=opts.get("system", ""),
148
+ disable_context_files=opts.get("no_context_files", False),
149
+ project_trusted=project_trusted,
150
+ )
151
+
152
+ runtime = await Runtime.create(config)
153
+
154
+ try:
155
+ match opts["mode"]:
156
+ case "interactive":
157
+ await _run_interactive(runtime, opts["theme"])
158
+ case "print":
159
+ await _run_print(runtime, opts["message"], quiet=opts.get("quiet", False))
160
+ case "json":
161
+ await _run_json(runtime, opts["message"], quiet=opts.get("quiet", False))
162
+ case "rpc":
163
+ from tau.rpc.mode import run_rpc_mode
164
+ await run_rpc_mode(runtime)
165
+ finally:
166
+ # Emit `runtime_stop` once, in every mode, on the way out — symmetric to
167
+ # the `runtime_ready` fired in Runtime.create.
168
+ await runtime.ashutdown()
169
+
170
+
171
+ async def _run_interactive(runtime: "Runtime", theme: str | None) -> None:
172
+ """Run the interactive TUI mode."""
173
+ from tau.tui.app import App
174
+ app = await App.create(runtime, theme=theme)
175
+ await app.run()
176
+
177
+
178
+ async def _run_print(runtime: "Runtime", message: str | None, quiet: bool = False) -> None:
179
+ """Run in print mode: send a message and print the response."""
180
+ if not message:
181
+ raise click.ClickException("A message is required in print mode. Usage: tau --print \"your prompt\"")
182
+
183
+ from tau.message.types import AssistantMessage
184
+ from tau.hooks.types import SettledEvent
185
+
186
+ result: AssistantMessage | None = None
187
+ settled = asyncio.Event()
188
+
189
+ async def on_message_end(event: object) -> None:
190
+ """Capture the final assistant message."""
191
+ nonlocal result
192
+ msg = getattr(event, "message", None)
193
+ if isinstance(msg, AssistantMessage):
194
+ result = msg
195
+
196
+ async def on_settled(_event: object) -> None:
197
+ """Signal that processing is complete."""
198
+ settled.set()
199
+
200
+ hooks = runtime.hooks
201
+ unsub_msg = hooks.register("message_end", on_message_end)
202
+ unsub_settled = hooks.register("settled", on_settled)
203
+
204
+ try:
205
+ await runtime.invoke(message)
206
+ await settled.wait()
207
+ finally:
208
+ unsub_msg()
209
+ unsub_settled()
210
+
211
+ if result is None:
212
+ raise click.ClickException("No response received.")
213
+
214
+ for content in result.contents:
215
+ if hasattr(content, "text"):
216
+ click.echo(content.text, nl=False)
217
+
218
+
219
+ async def _run_json(runtime: "Runtime", message: str | None, quiet: bool = False) -> None:
220
+ """Run in JSON mode: send a message and return structured JSON output."""
221
+ if not message:
222
+ raise click.ClickException("A message is required in json mode. Usage: tau --mode json \"your prompt\"")
223
+
224
+ import json
225
+ import dataclasses
226
+ from tau.hooks.types import SettledEvent
227
+
228
+ settled = asyncio.Event()
229
+
230
+ def _serialize(event: object) -> str:
231
+ if dataclasses.is_dataclass(event) and not isinstance(event, type):
232
+ return json.dumps(dataclasses.asdict(event))
233
+ return json.dumps({"type": type(event).__name__})
234
+
235
+ async def on_event(event: object) -> None:
236
+ """Output event as JSON and signal when settled."""
237
+ click.echo(_serialize(event))
238
+ if isinstance(event, SettledEvent):
239
+ settled.set()
240
+
241
+ hooks = runtime.hooks
242
+ hook_names = [
243
+ "agent_start", "agent_end", "message_start", "message_update",
244
+ "message_end", "tool_execution_start", "tool_execution_end",
245
+ "settled",
246
+ ]
247
+ unsubs = [hooks.register(name, on_event) for name in hook_names]
248
+
249
+ try:
250
+ await runtime.invoke(message)
251
+ await settled.wait()
252
+ finally:
253
+ for unsub in unsubs:
254
+ unsub()
255
+
256
+
257
+ cli.add_command(auth)
258
+ cli.add_command(install)
259
+ cli.add_command(remove)
260
+ cli.add_command(update)
261
+ cli.add_command(list_packages, name="list")
262
+
263
+
264
+ def main() -> None:
265
+ """Entry point for the CLI."""
266
+ cli()
File without changes
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import click
6
+
7
+
8
+ @click.group("auth", context_settings={"help_option_names": ["-h", "--help"]})
9
+ def auth():
10
+ """Manage API key credentials."""
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # list
15
+ # ---------------------------------------------------------------------------
16
+
17
+ @auth.command("list")
18
+ def auth_list():
19
+ """List all stored credentials with masked keys."""
20
+ data = _load()
21
+ if not data:
22
+ click.echo("No credentials stored.")
23
+ return
24
+ for provider, cred in data.items():
25
+ cred_type = cred.get("type", "?")
26
+ if cred_type == "api_key":
27
+ key = cred.get("key", "")
28
+ masked = key[:6] + "…" + key[-4:] if len(key) > 10 else "***"
29
+ click.echo(f" {provider:<24} api_key {masked}")
30
+ else:
31
+ click.echo(f" {provider:<24} {cred_type}")
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # set
36
+ # ---------------------------------------------------------------------------
37
+
38
+ @auth.command("set")
39
+ @click.argument("provider")
40
+ @click.argument("key")
41
+ def auth_set(provider, key):
42
+ """Store an API key for a PROVIDER."""
43
+ data = _load()
44
+ data[provider] = {"type": "api_key", "key": key}
45
+ _save(data)
46
+ click.echo(f"Saved API key for '{provider}'.")
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # unset
51
+ # ---------------------------------------------------------------------------
52
+
53
+ @auth.command("unset")
54
+ @click.argument("provider")
55
+ def auth_unset(provider):
56
+ """Remove stored credentials for a PROVIDER."""
57
+ data = _load()
58
+ if provider not in data:
59
+ raise click.ClickException(f"No credentials found for '{provider}'.")
60
+ del data[provider]
61
+ _save(data)
62
+ click.echo(f"Unset credentials for '{provider}'.")
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # status
67
+ # ---------------------------------------------------------------------------
68
+
69
+ @auth.command("status")
70
+ def auth_status():
71
+ """Show configuration status for all known providers."""
72
+ from tau.builtins.providers.text import api_providers, oauth_providers
73
+ from tau.auth.manager import AuthManager
74
+ from tau.inference.provider.registry import ProviderRegistry
75
+ from tau.inference.provider.types import OAuthProvider
76
+
77
+ registry = ProviderRegistry()
78
+ for p in api_providers + oauth_providers:
79
+ registry.text.register(p)
80
+
81
+ manager = AuthManager.create(registry)
82
+
83
+ all_providers = api_providers + oauth_providers
84
+ header = f" {'Provider':<24} {'Type':<8} {'Source':<8} Status"
85
+ separator = " " + "─" * 54
86
+ click.echo(header)
87
+ click.echo(separator)
88
+
89
+ for provider in all_providers:
90
+ status = manager.get_auth_status(provider.id)
91
+ ptype = "oauth" if isinstance(provider, OAuthProvider) else "api_key"
92
+
93
+ if status.configured:
94
+ source = click.style(f"{status.source:<8}", fg="cyan")
95
+ indicator = click.style("✓ configured", fg="green")
96
+ else:
97
+ source = click.style(f"{'—':<8}", fg="bright_black")
98
+ indicator = click.style("✗ not configured", fg="bright_black")
99
+
100
+ click.echo(f" {provider.id:<24} {ptype:<8} {source} {indicator}")
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # login
105
+ # ---------------------------------------------------------------------------
106
+
107
+ @auth.command("login")
108
+ @click.argument("provider")
109
+ def auth_login(provider):
110
+ """Start an OAuth login flow for a PROVIDER."""
111
+ import asyncio
112
+ asyncio.run(_login(provider))
113
+
114
+
115
+ async def _login(provider_id: str) -> None:
116
+ from tau.builtins.providers.text import oauth_providers
117
+ from tau.auth.manager import AuthManager
118
+ from tau.inference.provider.registry import ProviderRegistry
119
+ from tau.inference.provider.oauth.types import OAuthLoginCallbacks
120
+
121
+ oauth_ids = [p.id for p in oauth_providers]
122
+ if provider_id not in oauth_ids:
123
+ raise click.ClickException(
124
+ f"'{provider_id}' does not support OAuth. "
125
+ f"Use 'tau auth set {provider_id} <key>' instead."
126
+ )
127
+
128
+ registry = ProviderRegistry()
129
+ for p in oauth_providers:
130
+ registry.text.register(p)
131
+
132
+ manager = AuthManager.create(registry)
133
+
134
+ callbacks = OAuthLoginCallbacks(
135
+ on_url=lambda url: click.echo(f"Open this URL to authenticate:\n\n {url}\n"),
136
+ on_code=lambda: click.echo("Waiting for authentication…"),
137
+ )
138
+
139
+ click.echo(f"Logging in to '{provider_id}'…")
140
+ await manager.login(provider_id, callbacks)
141
+ click.echo(click.style(f"✓ Logged in to '{provider_id}'.", fg="green"))
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # logout
146
+ # ---------------------------------------------------------------------------
147
+
148
+ @auth.command("logout")
149
+ @click.argument("provider")
150
+ def auth_logout(provider):
151
+ """Revoke OAuth credentials for a PROVIDER."""
152
+ import asyncio
153
+ asyncio.run(_logout(provider))
154
+
155
+
156
+ async def _logout(provider_id: str) -> None:
157
+ from tau.builtins.providers.text import oauth_providers
158
+ from tau.auth.manager import AuthManager
159
+ from tau.inference.provider.registry import ProviderRegistry
160
+
161
+ registry = ProviderRegistry()
162
+ for p in oauth_providers:
163
+ registry.text.register(p)
164
+
165
+ manager = AuthManager.create(registry)
166
+
167
+ if not manager.has(provider_id):
168
+ raise click.ClickException(f"No stored credentials found for '{provider_id}'.")
169
+
170
+ await manager.logout(provider_id)
171
+ click.echo(click.style(f"✓ Logged out of '{provider_id}'.", fg="green"))
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Storage helpers
176
+ # ---------------------------------------------------------------------------
177
+
178
+ def _load() -> dict:
179
+ from tau.settings.paths import get_auth_path
180
+ path = get_auth_path()
181
+ if not path.exists():
182
+ return {}
183
+ try:
184
+ return json.loads(path.read_text())
185
+ except Exception:
186
+ return {}
187
+
188
+
189
+ def _save(data: dict) -> None:
190
+ from tau.settings.paths import get_auth_path
191
+ path = get_auth_path()
192
+ path.parent.mkdir(parents=True, exist_ok=True)
193
+ path.write_text(json.dumps(data, indent=2) + "\n")