thwip-cli 1.0.0__tar.gz
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.
- thwip_cli-1.0.0/.gitignore +47 -0
- thwip_cli-1.0.0/PKG-INFO +157 -0
- thwip_cli-1.0.0/README.md +121 -0
- thwip_cli-1.0.0/pyproject.toml +64 -0
- thwip_cli-1.0.0/tests/test_agents.py +40 -0
- thwip_cli-1.0.0/tests/test_detector.py +19 -0
- thwip_cli-1.0.0/tests/test_session.py +57 -0
- thwip_cli-1.0.0/tests/test_tools.py +40 -0
- thwip_cli-1.0.0/thwip/__init__.py +4 -0
- thwip_cli-1.0.0/thwip/__main__.py +6 -0
- thwip_cli-1.0.0/thwip/agents/__init__.py +98 -0
- thwip_cli-1.0.0/thwip/agents/base.py +343 -0
- thwip_cli-1.0.0/thwip/agents/claude_agent.py +346 -0
- thwip_cli-1.0.0/thwip/agents/deepseek_agent.py +217 -0
- thwip_cli-1.0.0/thwip/agents/google_agent.py +314 -0
- thwip_cli-1.0.0/thwip/agents/groq_agent.py +181 -0
- thwip_cli-1.0.0/thwip/agents/ollama_agent.py +180 -0
- thwip_cli-1.0.0/thwip/agents/openai_agent.py +338 -0
- thwip_cli-1.0.0/thwip/agents/openrouter_agent.py +192 -0
- thwip_cli-1.0.0/thwip/cli.py +593 -0
- thwip_cli-1.0.0/thwip/config.py +363 -0
- thwip_cli-1.0.0/thwip/detector.py +259 -0
- thwip_cli-1.0.0/thwip/limits.py +84 -0
- thwip_cli-1.0.0/thwip/session.py +175 -0
- thwip_cli-1.0.0/thwip/shortcuts.py +75 -0
- thwip_cli-1.0.0/thwip/theme.py +380 -0
- thwip_cli-1.0.0/thwip/tools/__init__.py +184 -0
- thwip_cli-1.0.0/thwip/tools/code_runner.py +62 -0
- thwip_cli-1.0.0/thwip/tools/file_editor.py +92 -0
- thwip_cli-1.0.0/thwip/tools/git_ops.py +44 -0
- thwip_cli-1.0.0/thwip/tools/terminal.py +65 -0
- thwip_cli-1.0.0/thwip/utils.py +110 -0
- thwip_cli-1.0.0/uv.lock +1222 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Python artifacts
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
env/
|
|
26
|
+
venv/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# Testing & linting
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.ruff_cache/
|
|
32
|
+
.coverage
|
|
33
|
+
htmlcov/
|
|
34
|
+
|
|
35
|
+
# Environment & local credentials
|
|
36
|
+
.env
|
|
37
|
+
.env.local
|
|
38
|
+
*.local
|
|
39
|
+
|
|
40
|
+
# System & editor
|
|
41
|
+
.DS_Store
|
|
42
|
+
.vscode/
|
|
43
|
+
.idea/
|
|
44
|
+
|
|
45
|
+
# Local context memory
|
|
46
|
+
context.md
|
|
47
|
+
|
thwip_cli-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: thwip-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Universal coding agent multiplexer: detect, switch, and route between AI coding agents seamlessly
|
|
5
|
+
Project-URL: Homepage, https://github.com/tanmayhutt/thwip-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/tanmayhutt/thwip-cli
|
|
7
|
+
Author-email: Tanmay <tanmay@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: ai,cli,coding-agent,llm,multiplexer
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: anthropic>=0.40
|
|
20
|
+
Requires-Dist: click>=8.0
|
|
21
|
+
Requires-Dist: google-genai>=1.0
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: openai>=1.50
|
|
24
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
26
|
+
Requires-Dist: tenacity>=9.0
|
|
27
|
+
Requires-Dist: tomli-w>=1.0
|
|
28
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: mistralai>=1.0; extra == 'all'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# thwip
|
|
38
|
+
|
|
39
|
+
> Universal Coding Agent Multiplexer
|
|
40
|
+
>
|
|
41
|
+
> Detect all installed AI coding agents on your device, combine them in a unified terminal interface, and hot-swap models mid-conversation when hitting rate limits without losing context.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Auto-Detection**: Discovers installed AI coding agents (Claude Code, Antigravity, Gemini CLI, OpenAI/Codex, Aider, Copilot, Cursor, Windsurf, Cline, Ollama) and configured credentials.
|
|
48
|
+
- **Context Portability**: Switch between Anthropic Claude, Google Gemini, OpenAI Codex, or local Ollama mid-project. Conversation history and working files transfer directly.
|
|
49
|
+
- **Dynamic UI**: Terminal interface adapts its status bar, capabilities, and theme based on the active provider.
|
|
50
|
+
- **Capability Disclaimers**: Highlights when an agent lacks specific capabilities such as file editing or code execution.
|
|
51
|
+
- **Rate Limit Failover**: Detects HTTP 429 errors or quota exhaustion and prompts instant switching to ready fallback models.
|
|
52
|
+
- **Tool Layer**: Shared file editing, code execution (Python, Node, Shell), and Git integration across connected models.
|
|
53
|
+
- **Session Persistence**: Save and resume sessions across projects with `/session save` and `/session load`.
|
|
54
|
+
- **Terminal Ergonomics**: Autocompletion (Tab), keyboard shortcuts (Ctrl+S, Ctrl+T, Ctrl+H), and streaming markdown rendering.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
### 1. Installation
|
|
61
|
+
|
|
62
|
+
Install in editable mode or via pip:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/thwip-cli/thwip.git
|
|
66
|
+
cd thwip
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Launching thwip
|
|
71
|
+
|
|
72
|
+
Start the interactive terminal in your current project directory:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
thwip
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Slash Commands and Shortcuts
|
|
81
|
+
|
|
82
|
+
| Command | Action |
|
|
83
|
+
|:---|:---|
|
|
84
|
+
| `/switch [agent] [model]` | Switch active agent or model mid-conversation |
|
|
85
|
+
| `/agents` | Show all detected coding agents, company status, and capabilities |
|
|
86
|
+
| `/models [agent]` | List available models for current or target agent |
|
|
87
|
+
| `/status` | Display current session, project, and token stats |
|
|
88
|
+
| `/limits` | View token usage, quota, and spend metrics |
|
|
89
|
+
| `/detect` | Re-scan system for newly installed coding agents |
|
|
90
|
+
| `/session save [name]` | Save current chat session |
|
|
91
|
+
| `/session load <name>` | Load a previously saved session |
|
|
92
|
+
| `/session list` | List all saved sessions |
|
|
93
|
+
| `/session clear` | Clear current conversation memory |
|
|
94
|
+
| `/history` | View conversation history with model attribution badges |
|
|
95
|
+
| `/cost` | Show estimated session and cumulative cost |
|
|
96
|
+
| `/project [path]` | View or change project working directory |
|
|
97
|
+
| `Ctrl + S` | Quick switch agent prompt |
|
|
98
|
+
| `Ctrl + T` | Show agent status |
|
|
99
|
+
| `Ctrl + H` | View history |
|
|
100
|
+
| `/quit` | Exit thwip |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Supported Companies and Agents
|
|
105
|
+
|
|
106
|
+
| Company | Agent | Capabilities |
|
|
107
|
+
|:---|:---|:---|
|
|
108
|
+
| Anthropic | Claude Code (Sonnet, Opus, Haiku) | Chat, File Edit, Code Run, Terminal, Git, Search |
|
|
109
|
+
| Google | Antigravity / Gemini CLI (2.5 Pro, 2.5 Flash) | Chat, File Edit, Code Run, Terminal, Browser, Search |
|
|
110
|
+
| OpenAI | Codex / ChatGPT (GPT-4.1, o3, o4-mini) | Chat, File Edit, Code Run, Terminal |
|
|
111
|
+
| DeepSeek | DeepSeek V3 / R1 Reasoner | Chat, File Edit, Code Run, Reasoning |
|
|
112
|
+
| Groq | Llama 3.3 70B, Mixtral | Chat, File Edit, Code Run |
|
|
113
|
+
| Ollama | Local Models (Llama 3.3, Qwen Coder, DeepSeek R1) | Chat, File Edit, Code Run (Local, Offline) |
|
|
114
|
+
| OpenRouter | Multi-Company Models | Gateway Routing |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Configuration (`~/.thwip/config.toml`)
|
|
119
|
+
|
|
120
|
+
thwip auto-detects existing API keys from environment variables and existing agent configs (`~/.claude.json`, `~/.gemini/config.json`). Configuration can also be set manually:
|
|
121
|
+
|
|
122
|
+
```toml
|
|
123
|
+
[defaults]
|
|
124
|
+
agent = "claude"
|
|
125
|
+
model = "claude-sonnet-4"
|
|
126
|
+
project = "."
|
|
127
|
+
theme = "dark"
|
|
128
|
+
stream = true
|
|
129
|
+
auto_save = true
|
|
130
|
+
|
|
131
|
+
[keys]
|
|
132
|
+
anthropic = "sk-ant-..."
|
|
133
|
+
google = "AIza..."
|
|
134
|
+
openai = "sk-..."
|
|
135
|
+
deepseek = "sk-..."
|
|
136
|
+
groq = "gsk_..."
|
|
137
|
+
openrouter = "sk-or-..."
|
|
138
|
+
|
|
139
|
+
[ollama]
|
|
140
|
+
host = "http://localhost:11434"
|
|
141
|
+
|
|
142
|
+
[fallback]
|
|
143
|
+
enabled = true
|
|
144
|
+
chain = [
|
|
145
|
+
"claude/claude-sonnet-4",
|
|
146
|
+
"google/gemini-2.5-pro",
|
|
147
|
+
"openai/gpt-4.1",
|
|
148
|
+
"deepseek/deepseek-chat",
|
|
149
|
+
"ollama/llama3.3"
|
|
150
|
+
]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT License
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# thwip
|
|
2
|
+
|
|
3
|
+
> Universal Coding Agent Multiplexer
|
|
4
|
+
>
|
|
5
|
+
> Detect all installed AI coding agents on your device, combine them in a unified terminal interface, and hot-swap models mid-conversation when hitting rate limits without losing context.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Auto-Detection**: Discovers installed AI coding agents (Claude Code, Antigravity, Gemini CLI, OpenAI/Codex, Aider, Copilot, Cursor, Windsurf, Cline, Ollama) and configured credentials.
|
|
12
|
+
- **Context Portability**: Switch between Anthropic Claude, Google Gemini, OpenAI Codex, or local Ollama mid-project. Conversation history and working files transfer directly.
|
|
13
|
+
- **Dynamic UI**: Terminal interface adapts its status bar, capabilities, and theme based on the active provider.
|
|
14
|
+
- **Capability Disclaimers**: Highlights when an agent lacks specific capabilities such as file editing or code execution.
|
|
15
|
+
- **Rate Limit Failover**: Detects HTTP 429 errors or quota exhaustion and prompts instant switching to ready fallback models.
|
|
16
|
+
- **Tool Layer**: Shared file editing, code execution (Python, Node, Shell), and Git integration across connected models.
|
|
17
|
+
- **Session Persistence**: Save and resume sessions across projects with `/session save` and `/session load`.
|
|
18
|
+
- **Terminal Ergonomics**: Autocompletion (Tab), keyboard shortcuts (Ctrl+S, Ctrl+T, Ctrl+H), and streaming markdown rendering.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
### 1. Installation
|
|
25
|
+
|
|
26
|
+
Install in editable mode or via pip:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/thwip-cli/thwip.git
|
|
30
|
+
cd thwip
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Launching thwip
|
|
35
|
+
|
|
36
|
+
Start the interactive terminal in your current project directory:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
thwip
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Slash Commands and Shortcuts
|
|
45
|
+
|
|
46
|
+
| Command | Action |
|
|
47
|
+
|:---|:---|
|
|
48
|
+
| `/switch [agent] [model]` | Switch active agent or model mid-conversation |
|
|
49
|
+
| `/agents` | Show all detected coding agents, company status, and capabilities |
|
|
50
|
+
| `/models [agent]` | List available models for current or target agent |
|
|
51
|
+
| `/status` | Display current session, project, and token stats |
|
|
52
|
+
| `/limits` | View token usage, quota, and spend metrics |
|
|
53
|
+
| `/detect` | Re-scan system for newly installed coding agents |
|
|
54
|
+
| `/session save [name]` | Save current chat session |
|
|
55
|
+
| `/session load <name>` | Load a previously saved session |
|
|
56
|
+
| `/session list` | List all saved sessions |
|
|
57
|
+
| `/session clear` | Clear current conversation memory |
|
|
58
|
+
| `/history` | View conversation history with model attribution badges |
|
|
59
|
+
| `/cost` | Show estimated session and cumulative cost |
|
|
60
|
+
| `/project [path]` | View or change project working directory |
|
|
61
|
+
| `Ctrl + S` | Quick switch agent prompt |
|
|
62
|
+
| `Ctrl + T` | Show agent status |
|
|
63
|
+
| `Ctrl + H` | View history |
|
|
64
|
+
| `/quit` | Exit thwip |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Supported Companies and Agents
|
|
69
|
+
|
|
70
|
+
| Company | Agent | Capabilities |
|
|
71
|
+
|:---|:---|:---|
|
|
72
|
+
| Anthropic | Claude Code (Sonnet, Opus, Haiku) | Chat, File Edit, Code Run, Terminal, Git, Search |
|
|
73
|
+
| Google | Antigravity / Gemini CLI (2.5 Pro, 2.5 Flash) | Chat, File Edit, Code Run, Terminal, Browser, Search |
|
|
74
|
+
| OpenAI | Codex / ChatGPT (GPT-4.1, o3, o4-mini) | Chat, File Edit, Code Run, Terminal |
|
|
75
|
+
| DeepSeek | DeepSeek V3 / R1 Reasoner | Chat, File Edit, Code Run, Reasoning |
|
|
76
|
+
| Groq | Llama 3.3 70B, Mixtral | Chat, File Edit, Code Run |
|
|
77
|
+
| Ollama | Local Models (Llama 3.3, Qwen Coder, DeepSeek R1) | Chat, File Edit, Code Run (Local, Offline) |
|
|
78
|
+
| OpenRouter | Multi-Company Models | Gateway Routing |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Configuration (`~/.thwip/config.toml`)
|
|
83
|
+
|
|
84
|
+
thwip auto-detects existing API keys from environment variables and existing agent configs (`~/.claude.json`, `~/.gemini/config.json`). Configuration can also be set manually:
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
[defaults]
|
|
88
|
+
agent = "claude"
|
|
89
|
+
model = "claude-sonnet-4"
|
|
90
|
+
project = "."
|
|
91
|
+
theme = "dark"
|
|
92
|
+
stream = true
|
|
93
|
+
auto_save = true
|
|
94
|
+
|
|
95
|
+
[keys]
|
|
96
|
+
anthropic = "sk-ant-..."
|
|
97
|
+
google = "AIza..."
|
|
98
|
+
openai = "sk-..."
|
|
99
|
+
deepseek = "sk-..."
|
|
100
|
+
groq = "gsk_..."
|
|
101
|
+
openrouter = "sk-or-..."
|
|
102
|
+
|
|
103
|
+
[ollama]
|
|
104
|
+
host = "http://localhost:11434"
|
|
105
|
+
|
|
106
|
+
[fallback]
|
|
107
|
+
enabled = true
|
|
108
|
+
chain = [
|
|
109
|
+
"claude/claude-sonnet-4",
|
|
110
|
+
"google/gemini-2.5-pro",
|
|
111
|
+
"openai/gpt-4.1",
|
|
112
|
+
"deepseek/deepseek-chat",
|
|
113
|
+
"ollama/llama3.3"
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT License
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "thwip-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Universal coding agent multiplexer: detect, switch, and route between AI coding agents seamlessly"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Tanmay", email = "tanmay@users.noreply.github.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "coding-agent", "cli", "llm", "multiplexer"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"rich>=13.0",
|
|
28
|
+
"prompt-toolkit>=3.0",
|
|
29
|
+
"httpx>=0.27",
|
|
30
|
+
"openai>=1.50",
|
|
31
|
+
"anthropic>=0.40",
|
|
32
|
+
"google-genai>=1.0",
|
|
33
|
+
"tomli>=2.0;python_version<'3.11'",
|
|
34
|
+
"tomli-w>=1.0",
|
|
35
|
+
"click>=8.0",
|
|
36
|
+
"tenacity>=9.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.0",
|
|
42
|
+
"pytest-asyncio>=0.24",
|
|
43
|
+
"ruff>=0.7",
|
|
44
|
+
]
|
|
45
|
+
all = [
|
|
46
|
+
"mistralai>=1.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
thwip = "thwip.cli:main"
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/tanmayhutt/thwip-cli"
|
|
54
|
+
Repository = "https://github.com/tanmayhutt/thwip-cli"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["thwip"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
target-version = "py311"
|
|
61
|
+
line-length = 120
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for agent registry and capabilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from thwip.agents import AgentRegistry
|
|
8
|
+
from thwip.agents.base import Capability
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_agent_registry_initialization():
|
|
12
|
+
registry = AgentRegistry()
|
|
13
|
+
agents = registry.list_agents()
|
|
14
|
+
assert len(agents) >= 7
|
|
15
|
+
|
|
16
|
+
claude = registry.get_agent("claude")
|
|
17
|
+
assert claude is not None
|
|
18
|
+
assert claude.company == "Anthropic"
|
|
19
|
+
assert Capability.FILE_EDIT in claude.capabilities
|
|
20
|
+
|
|
21
|
+
google = registry.get_agent("gemini")
|
|
22
|
+
assert google is not None
|
|
23
|
+
assert google.company == "Google"
|
|
24
|
+
|
|
25
|
+
openai = registry.get_agent("codex")
|
|
26
|
+
assert openai is not None
|
|
27
|
+
assert openai.company == "OpenAI"
|
|
28
|
+
|
|
29
|
+
ollama = registry.get_agent("ollama")
|
|
30
|
+
assert ollama is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_capability_comparison():
|
|
34
|
+
registry = AgentRegistry()
|
|
35
|
+
claude = registry.get_agent("claude")
|
|
36
|
+
groq = registry.get_agent("groq")
|
|
37
|
+
|
|
38
|
+
# Claude has GIT and BROWSER, Groq might not
|
|
39
|
+
missing = groq.get_missing_capabilities(claude.capabilities)
|
|
40
|
+
assert isinstance(missing, list)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for system detector.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from thwip.detector import SystemDetector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_system_detector_scan():
|
|
11
|
+
detector = SystemDetector()
|
|
12
|
+
tools = detector.scan_all()
|
|
13
|
+
assert isinstance(tools, list)
|
|
14
|
+
# Ensure tool objects have valid fields
|
|
15
|
+
for t in tools:
|
|
16
|
+
assert t.name
|
|
17
|
+
assert t.company
|
|
18
|
+
assert t.category
|
|
19
|
+
assert isinstance(t.capabilities, list)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for thwip session persistence and cross-agent context portability.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from thwip.session import Session, Message
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_session_message_flow():
|
|
13
|
+
session = Session(name="test-session")
|
|
14
|
+
session.add_user_message("Hello from user")
|
|
15
|
+
session.add_assistant_message("Hello back", agent_name="claude", model="claude-sonnet-4")
|
|
16
|
+
|
|
17
|
+
assert len(session.messages) == 2
|
|
18
|
+
assert session.messages[0].role == "user"
|
|
19
|
+
assert session.messages[0].content == "Hello from user"
|
|
20
|
+
assert session.messages[1].role == "assistant"
|
|
21
|
+
assert session.messages[1].agent_name == "claude"
|
|
22
|
+
|
|
23
|
+
# Verify portable representation
|
|
24
|
+
portable = session.to_portable_messages()
|
|
25
|
+
assert len(portable) == 2
|
|
26
|
+
assert portable[0] == {"role": "user", "content": "Hello from user"}
|
|
27
|
+
assert portable[1] == {"role": "assistant", "content": "Hello back"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_session_agent_switching():
|
|
31
|
+
session = Session(current_agent="claude", current_model="claude-sonnet-4")
|
|
32
|
+
session.add_user_message("Write a python function")
|
|
33
|
+
session.add_assistant_message("def hello(): pass", agent_name="claude", model="claude-sonnet-4")
|
|
34
|
+
|
|
35
|
+
# Switch agent to google/gemini
|
|
36
|
+
session.switch_agent("google", "gemini-2.5-pro")
|
|
37
|
+
|
|
38
|
+
assert session.current_agent == "google"
|
|
39
|
+
assert session.current_model == "gemini-2.5-pro"
|
|
40
|
+
# Context must be fully preserved
|
|
41
|
+
assert len(session.messages) == 2
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_session_serialization():
|
|
45
|
+
session = Session(name="serialize-test", project_path="/tmp/test")
|
|
46
|
+
session.add_user_message("How are you?")
|
|
47
|
+
session.add_assistant_message("Doing great!", agent_name="openai", model="gpt-4.1")
|
|
48
|
+
|
|
49
|
+
saved_path = session.save("serialize-test")
|
|
50
|
+
assert saved_path.is_file()
|
|
51
|
+
|
|
52
|
+
loaded = Session.load("serialize-test")
|
|
53
|
+
assert loaded is not None
|
|
54
|
+
assert loaded.name == "serialize-test"
|
|
55
|
+
assert loaded.project_path == "/tmp/test"
|
|
56
|
+
assert len(loaded.messages) == 2
|
|
57
|
+
assert loaded.messages[1].agent_name == "openai"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for file editor, code runner, and terminal tools.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from thwip.tools import ToolManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_file_editor_operations():
|
|
13
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
14
|
+
manager = ToolManager(project_path=tmpdir)
|
|
15
|
+
|
|
16
|
+
# 1. Write file
|
|
17
|
+
res = manager.file_editor.write_file("hello.txt", "line1\nline2\nline3")
|
|
18
|
+
assert "Successfully wrote" in res
|
|
19
|
+
|
|
20
|
+
# 2. Read file
|
|
21
|
+
content = manager.file_editor.read_file("hello.txt")
|
|
22
|
+
assert "line1\nline2\nline3" in content
|
|
23
|
+
|
|
24
|
+
# 3. Edit file
|
|
25
|
+
edit_res = manager.file_editor.edit_file("hello.txt", "line2", "line2_updated")
|
|
26
|
+
assert "Successfully updated" in edit_res
|
|
27
|
+
|
|
28
|
+
updated_content = manager.file_editor.read_file("hello.txt")
|
|
29
|
+
assert "line2_updated" in updated_content
|
|
30
|
+
|
|
31
|
+
# 4. List files
|
|
32
|
+
list_res = manager.file_editor.list_files()
|
|
33
|
+
assert "hello.txt" in list_res
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_code_runner_python():
|
|
37
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
38
|
+
manager = ToolManager(project_path=tmpdir)
|
|
39
|
+
res = manager.code_runner.run_python("print('Hello from Thwip')")
|
|
40
|
+
assert "Hello from Thwip" in res
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent registry and discovery for thwip.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Type
|
|
8
|
+
|
|
9
|
+
from thwip.agents.base import BaseAgent, Capability, LimitStatus, ModelInfo
|
|
10
|
+
from thwip.agents.claude_agent import ClaudeAgent
|
|
11
|
+
from thwip.agents.google_agent import GoogleAgent
|
|
12
|
+
from thwip.agents.openai_agent import OpenAIAgent
|
|
13
|
+
from thwip.agents.deepseek_agent import DeepSeekAgent
|
|
14
|
+
from thwip.agents.groq_agent import GroqAgent
|
|
15
|
+
from thwip.agents.ollama_agent import OllamaAgent
|
|
16
|
+
from thwip.agents.openrouter_agent import OpenRouterAgent
|
|
17
|
+
from thwip.config import ThwipConfig
|
|
18
|
+
|
|
19
|
+
# List of all available agent classes in order
|
|
20
|
+
ALL_AGENT_CLASSES: list[Type[BaseAgent]] = [
|
|
21
|
+
ClaudeAgent,
|
|
22
|
+
GoogleAgent,
|
|
23
|
+
OpenAIAgent,
|
|
24
|
+
DeepSeekAgent,
|
|
25
|
+
GroqAgent,
|
|
26
|
+
OllamaAgent,
|
|
27
|
+
OpenRouterAgent,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentRegistry:
|
|
32
|
+
"""Manages instantiated agents and dynamic lookup."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: ThwipConfig | None = None) -> None:
|
|
35
|
+
self.config = config or ThwipConfig.load()
|
|
36
|
+
self._agents: dict[str, BaseAgent] = {}
|
|
37
|
+
self._initialize_agents()
|
|
38
|
+
|
|
39
|
+
def _initialize_agents(self) -> None:
|
|
40
|
+
"""Instantiate all agent adapters with configured keys."""
|
|
41
|
+
for agent_cls in ALL_AGENT_CLASSES:
|
|
42
|
+
name = agent_cls.name
|
|
43
|
+
key = self.config.get_key(name)
|
|
44
|
+
if name == "google" and not key:
|
|
45
|
+
key = self.config.get_key("gemini")
|
|
46
|
+
if name == "claude" and not key:
|
|
47
|
+
key = self.config.get_key("anthropic")
|
|
48
|
+
if name == "openai" and not key:
|
|
49
|
+
key = self.config.get_key("codex")
|
|
50
|
+
|
|
51
|
+
if agent_cls is OllamaAgent:
|
|
52
|
+
inst = OllamaAgent(host=self.config.ollama_host)
|
|
53
|
+
else:
|
|
54
|
+
inst = agent_cls(api_key=key)
|
|
55
|
+
|
|
56
|
+
self._agents[name] = inst
|
|
57
|
+
|
|
58
|
+
def get_agent(self, name: str) -> BaseAgent | None:
|
|
59
|
+
"""Get agent by name or alias."""
|
|
60
|
+
alias_map = {
|
|
61
|
+
"claude": "claude",
|
|
62
|
+
"claude-code": "claude",
|
|
63
|
+
"anthropic": "claude",
|
|
64
|
+
"google": "google",
|
|
65
|
+
"gemini": "google",
|
|
66
|
+
"antigravity": "google",
|
|
67
|
+
"agy": "google",
|
|
68
|
+
"openai": "openai",
|
|
69
|
+
"codex": "openai",
|
|
70
|
+
"gpt": "openai",
|
|
71
|
+
"chatgpt": "openai",
|
|
72
|
+
"deepseek": "deepseek",
|
|
73
|
+
"groq": "groq",
|
|
74
|
+
"ollama": "ollama",
|
|
75
|
+
"local": "ollama",
|
|
76
|
+
"openrouter": "openrouter",
|
|
77
|
+
}
|
|
78
|
+
normalized = alias_map.get(name.lower().strip(), name.lower().strip())
|
|
79
|
+
return self._agents.get(normalized)
|
|
80
|
+
|
|
81
|
+
def list_agents(self) -> list[BaseAgent]:
|
|
82
|
+
"""Return all instantiated agents."""
|
|
83
|
+
return list(self._agents.values())
|
|
84
|
+
|
|
85
|
+
def get_ready_agents(self) -> list[BaseAgent]:
|
|
86
|
+
"""Return all agents that are installed and configured."""
|
|
87
|
+
return [
|
|
88
|
+
a for a in self._agents.values()
|
|
89
|
+
if a.is_installed() and a.is_configured()
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
def find_fallback_agent(self, current_agent_name: str) -> BaseAgent | None:
|
|
93
|
+
"""Find the next best ready agent for fallback."""
|
|
94
|
+
ready = self.get_ready_agents()
|
|
95
|
+
for a in ready:
|
|
96
|
+
if a.name != current_agent_name:
|
|
97
|
+
return a
|
|
98
|
+
return None
|