thwip-cli 1.0.3__tar.gz → 1.0.4__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.3 → thwip_cli-1.0.4}/PKG-INFO +1 -1
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/pyproject.toml +1 -1
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/__init__.py +1 -1
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/base.py +23 -1
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/claude_agent.py +62 -16
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/google_agent.py +40 -10
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/openai_agent.py +68 -9
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/cli.py +19 -9
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/theme.py +4 -2
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/uv.lock +1 -1
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/.github/workflows/publish.yml +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/.gitignore +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/README.md +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/install.sh +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/tests/test_agents.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/tests/test_detector.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/tests/test_session.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/tests/test_tools.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/__main__.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/__init__.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/deepseek_agent.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/groq_agent.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/ollama_agent.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/agents/openrouter_agent.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/config.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/detector.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/limits.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/session.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/shortcuts.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/tools/__init__.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/tools/code_runner.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/tools/file_editor.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/tools/git_ops.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/tools/terminal.py +0 -0
- {thwip_cli-1.0.3 → thwip_cli-1.0.4}/thwip/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thwip-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Universal coding agent multiplexer: detect, switch, and route between AI coding agents seamlessly
|
|
5
5
|
Project-URL: Homepage, https://github.com/tanmayhutt/thwip-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/tanmayhutt/thwip-cli
|
|
@@ -210,9 +210,22 @@ class BaseAgent(ABC):
|
|
|
210
210
|
|
|
211
211
|
@abstractmethod
|
|
212
212
|
def is_configured(self) -> bool:
|
|
213
|
-
"""Check if this agent has a valid API key / credentials."""
|
|
213
|
+
"""Check if this agent has a valid API key or OAuth/subscription credentials."""
|
|
214
214
|
...
|
|
215
215
|
|
|
216
|
+
def _has_cli_auth(self) -> dict[str, str] | None:
|
|
217
|
+
"""
|
|
218
|
+
Check if the agent's CLI tool has its own OAuth/subscription auth.
|
|
219
|
+
Returns dict with 'method' and 'account' keys if found, else None.
|
|
220
|
+
Subclasses should override this.
|
|
221
|
+
"""
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def auth_method(self) -> str:
|
|
226
|
+
"""Return the auth method in use: 'api_key', 'oauth', 'subscription', or 'none'."""
|
|
227
|
+
return "none"
|
|
228
|
+
|
|
216
229
|
@abstractmethod
|
|
217
230
|
def get_install_info(self) -> dict[str, str]:
|
|
218
231
|
"""
|
|
@@ -305,6 +318,15 @@ class BaseAgent(ABC):
|
|
|
305
318
|
if not self.is_configured():
|
|
306
319
|
return ("Installed (No Key)", "status.limited")
|
|
307
320
|
|
|
321
|
+
# Show auth method in status for non-API-key auth
|
|
322
|
+
auth = self.auth_method
|
|
323
|
+
if auth in ("oauth", "subscription"):
|
|
324
|
+
cli_auth = self._has_cli_auth()
|
|
325
|
+
account = cli_auth.get("account", "") if cli_auth else ""
|
|
326
|
+
if account:
|
|
327
|
+
return (f"Ready ({account})", "status.ready")
|
|
328
|
+
return ("Ready (CLI Auth)", "status.ready")
|
|
329
|
+
|
|
308
330
|
limit = self.check_limits()
|
|
309
331
|
if limit == LimitStatus.OK:
|
|
310
332
|
return ("Ready", "status.ready")
|
|
@@ -155,7 +155,11 @@ class ClaudeAgent(BaseAgent):
|
|
|
155
155
|
# --- Detection ---
|
|
156
156
|
|
|
157
157
|
def is_installed(self) -> bool:
|
|
158
|
-
"""Check if Claude Code CLI or Claude desktop app is installed
|
|
158
|
+
"""Check if Claude Code CLI or Claude desktop app is installed.
|
|
159
|
+
|
|
160
|
+
The ~/.claude directory alone is not sufficient since other IDEs
|
|
161
|
+
(like Antigravity) may create it.
|
|
162
|
+
"""
|
|
159
163
|
if self.is_configured():
|
|
160
164
|
return True
|
|
161
165
|
if shutil.which("claude") is not None:
|
|
@@ -163,13 +167,50 @@ class ClaudeAgent(BaseAgent):
|
|
|
163
167
|
app_paths = [
|
|
164
168
|
Path("/Applications/Claude.app"),
|
|
165
169
|
Path.home() / "Applications" / "Claude.app",
|
|
166
|
-
Path.home() / ".claude",
|
|
167
170
|
]
|
|
168
171
|
return any(p.exists() for p in app_paths)
|
|
169
172
|
|
|
173
|
+
def _has_cli_auth(self) -> dict[str, str] | None:
|
|
174
|
+
"""Check for Claude Code subscription auth.
|
|
175
|
+
|
|
176
|
+
Requires the `claude` CLI binary or Claude.app to be present.
|
|
177
|
+
The ~/.claude directory alone is not sufficient since other IDEs
|
|
178
|
+
(like Antigravity) may create it.
|
|
179
|
+
"""
|
|
180
|
+
has_cli = shutil.which("claude") is not None
|
|
181
|
+
has_app = Path("/Applications/Claude.app").exists() or (Path.home() / "Applications" / "Claude.app").exists()
|
|
182
|
+
|
|
183
|
+
if not has_cli and not has_app:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
claude_dir = Path.home() / ".claude"
|
|
187
|
+
if not claude_dir.is_dir():
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
# Claude Code stores session data when authenticated
|
|
191
|
+
auth_indicators = [
|
|
192
|
+
claude_dir / "projects",
|
|
193
|
+
claude_dir / "backups",
|
|
194
|
+
claude_dir / "debug",
|
|
195
|
+
]
|
|
196
|
+
has_sessions = any(p.is_dir() and any(p.iterdir()) for p in auth_indicators if p.exists())
|
|
197
|
+
|
|
198
|
+
if has_sessions:
|
|
199
|
+
return {"method": "subscription", "account": "Anthropic Max"}
|
|
200
|
+
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def auth_method(self) -> str:
|
|
205
|
+
if self._get_api_key():
|
|
206
|
+
return "api_key"
|
|
207
|
+
if self._has_cli_auth():
|
|
208
|
+
return "subscription"
|
|
209
|
+
return "none"
|
|
210
|
+
|
|
170
211
|
def is_configured(self) -> bool:
|
|
171
|
-
"""Check if we have a valid
|
|
172
|
-
return bool(self._get_api_key())
|
|
212
|
+
"""Check if we have a valid API key or subscription auth."""
|
|
213
|
+
return bool(self._get_api_key()) or bool(self._has_cli_auth())
|
|
173
214
|
|
|
174
215
|
def get_install_info(self) -> dict[str, str]:
|
|
175
216
|
"""Get Claude Code installation details."""
|
|
@@ -198,7 +239,6 @@ class ClaudeAgent(BaseAgent):
|
|
|
198
239
|
app_paths = [
|
|
199
240
|
Path("/Applications/Claude.app"),
|
|
200
241
|
Path.home() / "Applications" / "Claude.app",
|
|
201
|
-
Path.home() / ".claude",
|
|
202
242
|
]
|
|
203
243
|
for app in app_paths:
|
|
204
244
|
if app.exists():
|
|
@@ -213,29 +253,35 @@ class ClaudeAgent(BaseAgent):
|
|
|
213
253
|
def get_subscription_info(self) -> SubscriptionInfo:
|
|
214
254
|
"""
|
|
215
255
|
Determine subscription status.
|
|
216
|
-
|
|
217
|
-
the key prefix and check basic validity.
|
|
256
|
+
Checks API key first, then CLI subscription auth.
|
|
218
257
|
"""
|
|
219
258
|
key = self._get_api_key()
|
|
220
|
-
if
|
|
259
|
+
if key:
|
|
260
|
+
if key.startswith("sk-ant-"):
|
|
261
|
+
return SubscriptionInfo(
|
|
262
|
+
tier=SubscriptionTier.PRO,
|
|
263
|
+
is_active=True,
|
|
264
|
+
message="Anthropic API key detected",
|
|
265
|
+
)
|
|
221
266
|
return SubscriptionInfo(
|
|
222
267
|
tier=SubscriptionTier.UNKNOWN,
|
|
223
|
-
is_active=
|
|
224
|
-
message="
|
|
268
|
+
is_active=True,
|
|
269
|
+
message="API key detected",
|
|
225
270
|
)
|
|
226
271
|
|
|
227
|
-
|
|
228
|
-
if
|
|
272
|
+
cli_auth = self._has_cli_auth()
|
|
273
|
+
if cli_auth:
|
|
274
|
+
account = cli_auth.get("account", "Active")
|
|
229
275
|
return SubscriptionInfo(
|
|
230
|
-
tier=SubscriptionTier.PRO,
|
|
276
|
+
tier=SubscriptionTier.PRO,
|
|
231
277
|
is_active=True,
|
|
232
|
-
message="
|
|
278
|
+
message=f"Claude Code ({account})",
|
|
233
279
|
)
|
|
234
280
|
|
|
235
281
|
return SubscriptionInfo(
|
|
236
282
|
tier=SubscriptionTier.UNKNOWN,
|
|
237
|
-
is_active=
|
|
238
|
-
message="
|
|
283
|
+
is_active=False,
|
|
284
|
+
message="No credentials found",
|
|
239
285
|
)
|
|
240
286
|
|
|
241
287
|
# --- Chat ---
|
|
@@ -167,8 +167,31 @@ class GoogleAgent(BaseAgent):
|
|
|
167
167
|
]
|
|
168
168
|
return any(p.exists() for p in app_paths)
|
|
169
169
|
|
|
170
|
+
def _has_cli_auth(self) -> dict[str, str] | None:
|
|
171
|
+
"""Check for Google OAuth via Gemini CLI's google_accounts.json."""
|
|
172
|
+
acc_file = Path.home() / ".gemini" / "google_accounts.json"
|
|
173
|
+
if acc_file.is_file():
|
|
174
|
+
try:
|
|
175
|
+
data = json.loads(acc_file.read_text())
|
|
176
|
+
account = data.get("active") or ""
|
|
177
|
+
if not account and data.get("old"):
|
|
178
|
+
account = data["old"][0]
|
|
179
|
+
if account:
|
|
180
|
+
return {"method": "google_oauth", "account": account}
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def auth_method(self) -> str:
|
|
187
|
+
if self._get_api_key():
|
|
188
|
+
return "api_key"
|
|
189
|
+
if self._has_cli_auth():
|
|
190
|
+
return "oauth"
|
|
191
|
+
return "none"
|
|
192
|
+
|
|
170
193
|
def is_configured(self) -> bool:
|
|
171
|
-
return bool(self._get_api_key())
|
|
194
|
+
return bool(self._get_api_key()) or bool(self._has_cli_auth())
|
|
172
195
|
|
|
173
196
|
def get_install_info(self) -> dict[str, str]:
|
|
174
197
|
info: dict[str, str] = {"method": "not installed", "path": "", "version": ""}
|
|
@@ -220,25 +243,32 @@ class GoogleAgent(BaseAgent):
|
|
|
220
243
|
|
|
221
244
|
def get_subscription_info(self) -> SubscriptionInfo:
|
|
222
245
|
key = self._get_api_key()
|
|
223
|
-
if
|
|
246
|
+
if key:
|
|
247
|
+
if key.startswith("AIza"):
|
|
248
|
+
return SubscriptionInfo(
|
|
249
|
+
tier=SubscriptionTier.FREE,
|
|
250
|
+
is_active=True,
|
|
251
|
+
message="Google API key detected",
|
|
252
|
+
)
|
|
224
253
|
return SubscriptionInfo(
|
|
225
254
|
tier=SubscriptionTier.UNKNOWN,
|
|
226
|
-
is_active=
|
|
227
|
-
message="
|
|
255
|
+
is_active=True,
|
|
256
|
+
message="API key detected",
|
|
228
257
|
)
|
|
229
258
|
|
|
230
|
-
|
|
231
|
-
if
|
|
259
|
+
cli_auth = self._has_cli_auth()
|
|
260
|
+
if cli_auth:
|
|
261
|
+
account = cli_auth.get("account", "")
|
|
232
262
|
return SubscriptionInfo(
|
|
233
|
-
tier=SubscriptionTier.
|
|
263
|
+
tier=SubscriptionTier.PRO,
|
|
234
264
|
is_active=True,
|
|
235
|
-
message="Google
|
|
265
|
+
message=f"Google OAuth ({account})" if account else "Google OAuth active",
|
|
236
266
|
)
|
|
237
267
|
|
|
238
268
|
return SubscriptionInfo(
|
|
239
269
|
tier=SubscriptionTier.UNKNOWN,
|
|
240
|
-
is_active=
|
|
241
|
-
message="
|
|
270
|
+
is_active=False,
|
|
271
|
+
message="No credentials found",
|
|
242
272
|
)
|
|
243
273
|
|
|
244
274
|
# --- Chat ---
|
|
@@ -193,8 +193,47 @@ class OpenAIAgent(BaseAgent):
|
|
|
193
193
|
]
|
|
194
194
|
return any(p.exists() for p in app_paths)
|
|
195
195
|
|
|
196
|
+
def _has_cli_auth(self) -> dict[str, str] | None:
|
|
197
|
+
"""Check for ChatGPT OAuth via Codex CLI's auth.json."""
|
|
198
|
+
auth_file = Path.home() / ".codex" / "auth.json"
|
|
199
|
+
if auth_file.is_file():
|
|
200
|
+
try:
|
|
201
|
+
data = json.loads(auth_file.read_text())
|
|
202
|
+
if data.get("auth_mode") == "chatgpt" and data.get("tokens"):
|
|
203
|
+
tokens = data["tokens"]
|
|
204
|
+
result: dict[str, str] = {"method": "chatgpt_oauth"}
|
|
205
|
+
# Decode plan info from JWT id_token claims (no verification)
|
|
206
|
+
id_token = tokens.get("id_token", "")
|
|
207
|
+
if id_token:
|
|
208
|
+
try:
|
|
209
|
+
import base64
|
|
210
|
+
parts = id_token.split(".")
|
|
211
|
+
if len(parts) >= 2:
|
|
212
|
+
payload = parts[1]
|
|
213
|
+
padding = 4 - len(payload) % 4
|
|
214
|
+
if padding != 4:
|
|
215
|
+
payload += "=" * padding
|
|
216
|
+
claims = json.loads(base64.urlsafe_b64decode(payload))
|
|
217
|
+
result["account"] = claims.get("email", "")
|
|
218
|
+
auth_info = claims.get("https://api.openai.com/auth", {})
|
|
219
|
+
result["plan"] = auth_info.get("chatgpt_plan_type", "")
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
return result
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def auth_method(self) -> str:
|
|
229
|
+
if self._get_api_key():
|
|
230
|
+
return "api_key"
|
|
231
|
+
if self._has_cli_auth():
|
|
232
|
+
return "subscription"
|
|
233
|
+
return "none"
|
|
234
|
+
|
|
196
235
|
def is_configured(self) -> bool:
|
|
197
|
-
return bool(self._get_api_key())
|
|
236
|
+
return bool(self._get_api_key()) or bool(self._has_cli_auth())
|
|
198
237
|
|
|
199
238
|
def get_install_info(self) -> dict[str, str]:
|
|
200
239
|
info: dict[str, str] = {"method": "not installed", "path": "", "version": ""}
|
|
@@ -233,24 +272,44 @@ class OpenAIAgent(BaseAgent):
|
|
|
233
272
|
|
|
234
273
|
def get_subscription_info(self) -> SubscriptionInfo:
|
|
235
274
|
key = self._get_api_key()
|
|
236
|
-
if
|
|
275
|
+
if key:
|
|
276
|
+
if key.startswith("sk-"):
|
|
277
|
+
return SubscriptionInfo(
|
|
278
|
+
tier=SubscriptionTier.PRO,
|
|
279
|
+
is_active=True,
|
|
280
|
+
message="OpenAI API key detected",
|
|
281
|
+
)
|
|
237
282
|
return SubscriptionInfo(
|
|
238
283
|
tier=SubscriptionTier.UNKNOWN,
|
|
239
|
-
is_active=
|
|
240
|
-
message="
|
|
284
|
+
is_active=True,
|
|
285
|
+
message="API key detected",
|
|
241
286
|
)
|
|
242
287
|
|
|
243
|
-
|
|
288
|
+
cli_auth = self._has_cli_auth()
|
|
289
|
+
if cli_auth:
|
|
290
|
+
plan = cli_auth.get("plan", "")
|
|
291
|
+
account = cli_auth.get("account", "")
|
|
292
|
+
tier_map = {
|
|
293
|
+
"plus": SubscriptionTier.PRO,
|
|
294
|
+
"pro": SubscriptionTier.PRO,
|
|
295
|
+
"team": SubscriptionTier.TEAM,
|
|
296
|
+
"enterprise": SubscriptionTier.ENTERPRISE,
|
|
297
|
+
}
|
|
298
|
+
tier = tier_map.get(plan, SubscriptionTier.UNKNOWN)
|
|
299
|
+
plan_label = plan.title() if plan else "Active"
|
|
300
|
+
msg = f"ChatGPT {plan_label}"
|
|
301
|
+
if account:
|
|
302
|
+
msg += f" ({account})"
|
|
244
303
|
return SubscriptionInfo(
|
|
245
|
-
tier=
|
|
304
|
+
tier=tier,
|
|
246
305
|
is_active=True,
|
|
247
|
-
message=
|
|
306
|
+
message=msg,
|
|
248
307
|
)
|
|
249
308
|
|
|
250
309
|
return SubscriptionInfo(
|
|
251
310
|
tier=SubscriptionTier.UNKNOWN,
|
|
252
|
-
is_active=
|
|
253
|
-
message="
|
|
311
|
+
is_active=False,
|
|
312
|
+
message="No credentials found",
|
|
254
313
|
)
|
|
255
314
|
|
|
256
315
|
# --- Chat ---
|
|
@@ -107,6 +107,8 @@ class ThwipCLI:
|
|
|
107
107
|
"""Main async REPL loop."""
|
|
108
108
|
# 1. Detect agents on system
|
|
109
109
|
detected = self.detector.scan_all()
|
|
110
|
+
all_agents = self.registry.list_agents()
|
|
111
|
+
installed_count = len([a for a in all_agents if a.is_installed()])
|
|
110
112
|
ready_count = len(self.registry.get_ready_agents())
|
|
111
113
|
|
|
112
114
|
# 2. Display startup banner
|
|
@@ -118,7 +120,7 @@ class ThwipCLI:
|
|
|
118
120
|
model=self.session.current_model,
|
|
119
121
|
project_path=os.path.abspath(self.session.project_path),
|
|
120
122
|
session_name=self.session.name,
|
|
121
|
-
agents_detected=
|
|
123
|
+
agents_detected=installed_count,
|
|
122
124
|
agents_ready=ready_count,
|
|
123
125
|
)
|
|
124
126
|
)
|
|
@@ -326,15 +328,19 @@ class ThwipCLI:
|
|
|
326
328
|
async def cmd_switch(self, agent_name: str, model_id: str = "") -> None:
|
|
327
329
|
"""Switch to a different agent and/or model."""
|
|
328
330
|
if not agent_name:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
+
all_agents = self.registry.list_agents()
|
|
332
|
+
installed = [a for a in all_agents if a.is_installed()]
|
|
331
333
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
other_agents = [a for a in agents if not a.is_installed()]
|
|
334
|
+
if not installed:
|
|
335
|
+
print_error("No agents detected on your machine.")
|
|
336
|
+
return
|
|
336
337
|
|
|
337
|
-
|
|
338
|
+
console.print("\n[bold white]Available Coding Agents on Your Machine:[/bold white]")
|
|
339
|
+
|
|
340
|
+
# Sort: configured first, then just installed
|
|
341
|
+
ready_agents = [a for a in installed if a.is_configured()]
|
|
342
|
+
unready_agents = [a for a in installed if not a.is_configured()]
|
|
343
|
+
ordered = ready_agents + unready_agents
|
|
338
344
|
|
|
339
345
|
for i, a in enumerate(ordered, 1):
|
|
340
346
|
brand = get_brand(a.company)
|
|
@@ -347,10 +353,14 @@ class ThwipCLI:
|
|
|
347
353
|
f"({a.company}) - [{status_style}][{status_str}][/{status_style}][dim]{loc}[/dim]"
|
|
348
354
|
)
|
|
349
355
|
|
|
356
|
+
not_installed = len(all_agents) - len(installed)
|
|
357
|
+
if not_installed > 0:
|
|
358
|
+
console.print(f"\n [dim]{not_installed} other providers available (DeepSeek, Groq, Ollama, OpenRouter). Use /agents to see all.[/dim]")
|
|
359
|
+
|
|
350
360
|
choice = input(f"\nEnter choice [1-{len(ordered)}]: ").strip()
|
|
351
361
|
if choice.isdigit() and 1 <= int(choice) <= len(ordered):
|
|
352
362
|
agent_name = ordered[int(choice) - 1].name
|
|
353
|
-
elif choice.lower() in [a.name for a in
|
|
363
|
+
elif choice.lower() in [a.name for a in installed]:
|
|
354
364
|
agent_name = choice.lower()
|
|
355
365
|
else:
|
|
356
366
|
print_warning("Switch cancelled.")
|
|
@@ -16,6 +16,8 @@ from rich.table import Table
|
|
|
16
16
|
from rich.text import Text
|
|
17
17
|
from rich.theme import Theme
|
|
18
18
|
from rich.markdown import Markdown
|
|
19
|
+
|
|
20
|
+
from thwip import __version__
|
|
19
21
|
from rich.syntax import Syntax
|
|
20
22
|
from rich import box
|
|
21
23
|
|
|
@@ -151,7 +153,7 @@ def render_startup_banner(
|
|
|
151
153
|
lines.append("╚═╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝╚═╝ \n\n", style="dim")
|
|
152
154
|
|
|
153
155
|
lines.append(" Universal Coding Agent Multiplexer", style="bold white")
|
|
154
|
-
lines.append("
|
|
156
|
+
lines.append(f" v{__version__}\n\n", style="dim")
|
|
155
157
|
lines.append(" Agent: ", style="dim")
|
|
156
158
|
lines.append(f"{agent_name}", style=brand.label_style)
|
|
157
159
|
lines.append(f" ({company})\n", style="dim")
|
|
@@ -163,7 +165,7 @@ def render_startup_banner(
|
|
|
163
165
|
lines.append(f"{session_name}\n", style="white")
|
|
164
166
|
lines.append(" Environment: ", style="dim")
|
|
165
167
|
lines.append(f"{agents_detected}", style="bold white")
|
|
166
|
-
lines.append("
|
|
168
|
+
lines.append(" agents installed, ", style="dim")
|
|
167
169
|
lines.append(f"{agents_ready}", style="success")
|
|
168
170
|
lines.append(" ready\n\n", style="dim")
|
|
169
171
|
lines.append(" Quickstart: Type ", style="dim")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|