thwip-cli 1.0.2__tar.gz → 1.0.3__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.
Files changed (35) hide show
  1. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/PKG-INFO +1 -1
  2. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/pyproject.toml +1 -1
  3. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/__init__.py +1 -1
  4. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/base.py +2 -0
  5. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/claude_agent.py +6 -0
  6. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/google_agent.py +30 -1
  7. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/openai_agent.py +29 -6
  8. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/cli.py +137 -24
  9. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/.github/workflows/publish.yml +0 -0
  10. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/.gitignore +0 -0
  11. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/README.md +0 -0
  12. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/install.sh +0 -0
  13. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/tests/test_agents.py +0 -0
  14. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/tests/test_detector.py +0 -0
  15. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/tests/test_session.py +0 -0
  16. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/tests/test_tools.py +0 -0
  17. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/__main__.py +0 -0
  18. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/__init__.py +0 -0
  19. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/deepseek_agent.py +0 -0
  20. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/groq_agent.py +0 -0
  21. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/ollama_agent.py +0 -0
  22. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/agents/openrouter_agent.py +0 -0
  23. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/config.py +0 -0
  24. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/detector.py +0 -0
  25. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/limits.py +0 -0
  26. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/session.py +0 -0
  27. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/shortcuts.py +0 -0
  28. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/theme.py +0 -0
  29. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/tools/__init__.py +0 -0
  30. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/tools/code_runner.py +0 -0
  31. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/tools/file_editor.py +0 -0
  32. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/tools/git_ops.py +0 -0
  33. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/tools/terminal.py +0 -0
  34. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/thwip/utils.py +0 -0
  35. {thwip_cli-1.0.2 → thwip_cli-1.0.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thwip-cli
3
- Version: 1.0.2
3
+ Version: 1.0.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thwip-cli"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "Universal coding agent multiplexer: detect, switch, and route between AI coding agents seamlessly"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,4 @@
1
1
  """thwip: Universal Coding Agent Multiplexer."""
2
2
 
3
- __version__ = "1.0.2"
3
+ __version__ = "1.0.3"
4
4
  __app_name__ = "thwip"
@@ -158,6 +158,8 @@ class ModelInfo:
158
158
  """Information about a specific model."""
159
159
  id: str # "claude-sonnet-4"
160
160
  name: str # "Claude Sonnet 4"
161
+ tier: str = "balanced" # "flagship" (high), "balanced" (mid), "fast" (low-cost)
162
+ description: str = "" # e.g. "Flagship reasoning with 1M context"
161
163
  context_window: int = 0 # Max tokens
162
164
  max_output: int = 0 # Max output tokens
163
165
  supports_tools: bool = True
@@ -59,6 +59,8 @@ class ClaudeAgent(BaseAgent):
59
59
  ModelInfo(
60
60
  id="claude-sonnet-4",
61
61
  name="Claude Sonnet 4",
62
+ tier="flagship",
63
+ description="Flagship coding intelligence with hybrid thinking and tool precision",
62
64
  context_window=200_000,
63
65
  max_output=16_384,
64
66
  supports_tools=True,
@@ -72,6 +74,8 @@ class ClaudeAgent(BaseAgent):
72
74
  ModelInfo(
73
75
  id="claude-opus-4",
74
76
  name="Claude Opus 4",
77
+ tier="flagship",
78
+ description="Ultra high capability reasoning for complex systems engineering",
75
79
  context_window=200_000,
76
80
  max_output=32_000,
77
81
  supports_tools=True,
@@ -84,6 +88,8 @@ class ClaudeAgent(BaseAgent):
84
88
  ModelInfo(
85
89
  id="claude-haiku-3.5",
86
90
  name="Claude Haiku 3.5",
91
+ tier="fast",
92
+ description="Fastest lightweight Claude model for rapid iteration",
87
93
  context_window=200_000,
88
94
  max_output=8_192,
89
95
  supports_tools=True,
@@ -59,6 +59,8 @@ class GoogleAgent(BaseAgent):
59
59
  ModelInfo(
60
60
  id="gemini-2.5-pro",
61
61
  name="Gemini 2.5 Pro",
62
+ tier="flagship",
63
+ description="Flagship multimodal model with 1M context and advanced reasoning",
62
64
  context_window=1_048_576,
63
65
  max_output=65_536,
64
66
  supports_tools=True,
@@ -72,6 +74,8 @@ class GoogleAgent(BaseAgent):
72
74
  ModelInfo(
73
75
  id="gemini-2.5-flash",
74
76
  name="Gemini 2.5 Flash",
77
+ tier="balanced",
78
+ description="High-speed balanced model optimized for daily coding workflows",
75
79
  context_window=1_048_576,
76
80
  max_output=65_536,
77
81
  supports_tools=True,
@@ -81,6 +85,20 @@ class GoogleAgent(BaseAgent):
81
85
  pricing_input=0.15,
82
86
  pricing_output=0.60,
83
87
  ),
88
+ ModelInfo(
89
+ id="gemini-2.5-flash-lite",
90
+ name="Gemini 2.5 Flash Lite",
91
+ tier="fast",
92
+ description="Ultra lightweight and cost-effective model for rapid tasks",
93
+ context_window=1_048_576,
94
+ max_output=65_536,
95
+ supports_tools=True,
96
+ supports_streaming=True,
97
+ supports_vision=False,
98
+ supports_thinking=False,
99
+ pricing_input=0.075,
100
+ pricing_output=0.30,
101
+ ),
84
102
  ]
85
103
 
86
104
  def __init__(self, api_key: str | None = None) -> None:
@@ -171,7 +189,7 @@ class GoogleAgent(BaseAgent):
171
189
  pass
172
190
  break
173
191
 
174
- # Also check for Antigravity IDE or Gemini app (macOS app)
192
+ # Check for Antigravity IDE or Gemini app (macOS app)
175
193
  if not info["path"]:
176
194
  app_paths = [
177
195
  Path("/Applications/Antigravity IDE.app"),
@@ -187,6 +205,17 @@ class GoogleAgent(BaseAgent):
187
205
  info["version"] = "Installed"
188
206
  break
189
207
 
208
+ # Check for Google Account in ~/.gemini/google_accounts.json
209
+ acc_file = Path.home() / ".gemini" / "google_accounts.json"
210
+ if acc_file.is_file():
211
+ try:
212
+ acc_data = json.loads(acc_file.read_text())
213
+ account = acc_data.get("active") or (acc_data.get("old") and acc_data["old"][0]) or ""
214
+ if account:
215
+ info["account"] = account
216
+ except Exception:
217
+ pass
218
+
190
219
  return info
191
220
 
192
221
  def get_subscription_info(self) -> SubscriptionInfo:
@@ -40,9 +40,9 @@ class OpenAIAgent(BaseAgent):
40
40
  """
41
41
 
42
42
  name = "openai"
43
- display_name = "Codex / OpenAI"
43
+ display_name = "ChatGPT / Codex"
44
44
  company = "OpenAI"
45
- description = "OpenAI's coding agent with sandboxed code execution"
45
+ description = "OpenAI's ChatGPT and Codex models with tool execution"
46
46
  website = "https://platform.openai.com"
47
47
 
48
48
  capabilities = {
@@ -59,6 +59,8 @@ class OpenAIAgent(BaseAgent):
59
59
  ModelInfo(
60
60
  id="gpt-4.1",
61
61
  name="GPT-4.1",
62
+ tier="flagship",
63
+ description="Next-generation flagship with 1M context and advanced coding",
62
64
  context_window=1_047_576,
63
65
  max_output=32_768,
64
66
  supports_tools=True,
@@ -71,6 +73,8 @@ class OpenAIAgent(BaseAgent):
71
73
  ModelInfo(
72
74
  id="o3",
73
75
  name="O3",
76
+ tier="flagship",
77
+ description="Deep reasoning model with autonomous multi-step problem solving",
74
78
  context_window=200_000,
75
79
  max_output=100_000,
76
80
  supports_tools=True,
@@ -79,9 +83,24 @@ class OpenAIAgent(BaseAgent):
79
83
  pricing_input=2.0,
80
84
  pricing_output=8.0,
81
85
  ),
86
+ ModelInfo(
87
+ id="gpt-4o",
88
+ name="GPT-4o",
89
+ tier="balanced",
90
+ description="High-speed multimodal flagship for omni-modal coding and vision",
91
+ context_window=128_000,
92
+ max_output=16_384,
93
+ supports_tools=True,
94
+ supports_streaming=True,
95
+ supports_vision=True,
96
+ pricing_input=2.50,
97
+ pricing_output=10.0,
98
+ ),
82
99
  ModelInfo(
83
100
  id="o4-mini",
84
101
  name="O4 Mini",
102
+ tier="balanced",
103
+ description="Fast and affordable reasoning model for everyday programming",
85
104
  context_window=200_000,
86
105
  max_output=100_000,
87
106
  supports_tools=True,
@@ -91,19 +110,23 @@ class OpenAIAgent(BaseAgent):
91
110
  pricing_output=4.40,
92
111
  ),
93
112
  ModelInfo(
94
- id="gpt-4o",
95
- name="GPT-4o",
113
+ id="gpt-4o-mini",
114
+ name="GPT-4o Mini",
115
+ tier="fast",
116
+ description="Lightweight and low-latency model for fast script tasks",
96
117
  context_window=128_000,
97
118
  max_output=16_384,
98
119
  supports_tools=True,
99
120
  supports_streaming=True,
100
121
  supports_vision=True,
101
- pricing_input=2.50,
102
- pricing_output=10.0,
122
+ pricing_input=0.15,
123
+ pricing_output=0.60,
103
124
  ),
104
125
  ModelInfo(
105
126
  id="codex-mini",
106
127
  name="Codex Mini",
128
+ tier="fast",
129
+ description="Specialized fast code generation model",
107
130
  context_window=200_000,
108
131
  max_output=16_384,
109
132
  supports_tools=True,
@@ -195,8 +195,11 @@ class ThwipCLI:
195
195
  elif cmd in ("/agents", "/list"):
196
196
  self.cmd_show_agents()
197
197
 
198
+ elif cmd in ("/key", "/auth", "/config"):
199
+ self.cmd_auth_config(arg1, arg2)
200
+
198
201
  elif cmd in ("/models", "/m"):
199
- self.cmd_show_models(arg1)
202
+ self.cmd_show_models(arg1, arg2)
200
203
 
201
204
  elif cmd == "/tools":
202
205
  self.cmd_show_tools()
@@ -298,7 +301,8 @@ class ThwipCLI:
298
301
  ("/about", "Display full About section, architecture, and navigation guide"),
299
302
  ("/switch [agent] [model]", "Switch active agent/model mid-conversation without losing context"),
300
303
  ("/agents", "Show all detected coding agents, company status & capabilities"),
301
- ("/models [agent]", "List available models for current or target agent"),
304
+ ("/models [agent|tier]", "List models filtered by provider or tier (flagship, balanced, fast)"),
305
+ ("/key [provider] [key]", "View or configure API keys saved in ~/.thwip/config.toml"),
302
306
  ("/tools", "List all universal file, terminal, and git tools"),
303
307
  ("/status", "Display current session, project, and token stats"),
304
308
  ("/limits", "View token usage, quota, and spend metrics"),
@@ -397,33 +401,142 @@ class ThwipCLI:
397
401
  rows = [a.to_table_row() for a in self.registry.list_agents()]
398
402
  console.print(render_agents_table(rows))
399
403
 
400
- def cmd_show_models(self, agent_name: str = "") -> None:
401
- """List models for current or target agent."""
402
- target = self.registry.get_agent(agent_name) if agent_name else self.current_agent
403
- if not target:
404
- print_error(f"Agent '{agent_name}' not found.")
405
- return
404
+ def cmd_show_models(self, arg1: str = "", arg2: str = "") -> None:
405
+ """List models by provider or tier (flagship, balanced, fast)."""
406
+ tier_filter = ""
407
+ agent_target = None
408
+
409
+ tier_aliases = {
410
+ "flagship": "flagship",
411
+ "high": "flagship",
412
+ "pro": "flagship",
413
+ "balanced": "balanced",
414
+ "mid": "balanced",
415
+ "flash": "balanced",
416
+ "fast": "fast",
417
+ "low": "fast",
418
+ "lite": "fast",
419
+ "mini": "fast",
420
+ }
421
+
422
+ if arg1.lower() in tier_aliases:
423
+ tier_filter = tier_aliases[arg1.lower()]
424
+ elif arg1:
425
+ agent_target = self.registry.get_agent(arg1)
426
+ if not agent_target:
427
+ print_error(f"Agent '{arg1}' not found.")
428
+ return
429
+ if arg2.lower() in tier_aliases:
430
+ tier_filter = tier_aliases[arg2.lower()]
431
+
432
+ if not agent_target and not tier_filter:
433
+ agent_target = self.current_agent
406
434
 
407
- table = Table(title=f"Available Models for {target.display_name}", box=box.ROUNDED)
435
+ if agent_target:
436
+ agents_to_show = [agent_target]
437
+ title = f"Available Models for {agent_target.display_name}"
438
+ if tier_filter:
439
+ title += f" ({tier_filter.title()} Tier)"
440
+ else:
441
+ agents_to_show = self.registry.list_agents()
442
+ title = f"All {tier_filter.title()} Tier Models Across Providers"
443
+
444
+ table = Table(title=title, box=box.ROUNDED)
445
+ if not agent_target:
446
+ table.add_column("Provider", style="cyan")
408
447
  table.add_column("Model ID", style="bold white")
409
- table.add_column("Name", style="cyan")
410
- table.add_column("Context", style="white")
411
- table.add_column("Tools", style="green")
448
+ table.add_column("Name", style="white")
449
+ table.add_column("Tier", style="bold")
450
+ table.add_column("Context", style="dim")
412
451
  table.add_column("Thinking", style="magenta")
413
- table.add_column("Price (In/Out per 1M)", style="dim")
452
+ table.add_column("Price (In/Out 1M)", style="green")
453
+
454
+ tier_styles = {
455
+ "flagship": "[bold magenta]Flagship / High[/bold magenta]",
456
+ "balanced": "[bold cyan]Balanced / Mid[/bold cyan]",
457
+ "fast": "[bold green]Fast / Low[/bold green]",
458
+ }
459
+
460
+ for ag in agents_to_show:
461
+ for m in ag.available_models:
462
+ m_tier = getattr(m, "tier", "balanced")
463
+ if tier_filter and m_tier != tier_filter:
464
+ continue
465
+
466
+ ctx = f"{m.context_window:,}" if m.context_window else "-"
467
+ price = f"${m.pricing_input} / ${m.pricing_output}" if m.pricing_input else "Free"
468
+ tier_badge = tier_styles.get(m_tier, m_tier.title())
469
+ def_mark = " [dim](default)[/dim]" if m.is_default else ""
470
+
471
+ row = []
472
+ if not agent_target:
473
+ row.append(ag.company)
474
+ row.extend([
475
+ m.id + def_mark,
476
+ m.name,
477
+ tier_badge,
478
+ ctx,
479
+ "yes" if m.supports_thinking else "-",
480
+ price,
481
+ ])
482
+ table.add_row(*row)
483
+
484
+ console.print(table)
485
+ console.print("[dim]Filter by tier: [bold]/models flagship[/bold], [bold]/models balanced[/bold], [bold]/models fast[/bold][/dim]\n")
486
+
487
+ def cmd_auth_config(self, provider: str = "", key: str = "") -> None:
488
+ """View or set API credentials stored in ~/.thwip/config.toml."""
489
+ provider_map = {
490
+ "google": "google",
491
+ "gemini": "google",
492
+ "antigravity": "google",
493
+ "openai": "openai",
494
+ "chatgpt": "openai",
495
+ "codex": "openai",
496
+ "claude": "anthropic",
497
+ "anthropic": "anthropic",
498
+ "deepseek": "deepseek",
499
+ "groq": "groq",
500
+ "openrouter": "openrouter",
501
+ }
502
+
503
+ if provider and key:
504
+ target_prov = provider_map.get(provider.lower(), provider.lower())
505
+ self.config.keys[target_prov] = key
506
+ self.config.key_sources[target_prov] = "config.toml"
507
+ self.config.save()
508
+
509
+ # Refresh agent registry
510
+ self.registry = AgentRegistry(self.config)
511
+ agent = self.registry.get_agent(self.session.current_agent)
512
+ if agent:
513
+ self.current_agent = agent
514
+ print_success(f"API key for '{target_prov}' saved to ~/.thwip/config.toml")
515
+ return
516
+
517
+ # Show interactive auth overview
518
+ table = Table(title="thwip API Credentials & Authentication", box=box.ROUNDED)
519
+ table.add_column("Provider", style="bold white")
520
+ table.add_column("Auth Status", style="white")
521
+ table.add_column("Key Source", style="dim")
522
+ table.add_column("Config Command", style="cyan")
523
+
524
+ for prov, label in [
525
+ ("google", "Google Gemini / Antigravity"),
526
+ ("openai", "OpenAI / ChatGPT / Codex"),
527
+ ("anthropic", "Anthropic Claude"),
528
+ ("deepseek", "DeepSeek"),
529
+ ("groq", "Groq"),
530
+ ("openrouter", "OpenRouter"),
531
+ ]:
532
+ has_key = bool(self.config.keys.get(prov))
533
+ source = self.config.key_sources.get(prov, "none")
534
+ status = "[bold green]Configured[/bold green]" if has_key else "[bold yellow]Missing[/bold yellow]"
535
+ cmd_hint = f"/key {prov} <your_api_key>"
536
+ table.add_row(label, status, source, cmd_hint)
414
537
 
415
- for m in target.available_models:
416
- ctx = f"{m.context_window:,}" if m.context_window else "-"
417
- price = f"${m.pricing_input} / ${m.pricing_output}" if m.pricing_input else "Free / Local"
418
- table.add_row(
419
- m.id + (" (default)" if m.is_default else ""),
420
- m.name,
421
- ctx,
422
- "yes" if m.supports_tools else "-",
423
- "yes" if m.supports_thinking else "-",
424
- price,
425
- )
426
538
  console.print(table)
539
+ console.print("[dim]To set a key: [bold white]/key <provider> <key>[/bold white] (e.g. /key google AIzaSy...)[/dim]\n")
427
540
 
428
541
  def cmd_show_status(self) -> None:
429
542
  """Display status."""
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