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
|
@@ -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)
|
tau/commands/__init__.py
ADDED
tau/commands/registry.py
ADDED
|
@@ -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
|
tau/console/__init__.py
ADDED
|
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")
|