superqode 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,819 @@
1
+ """
2
+ Provider CLI commands for SuperQode.
3
+
4
+ Commands for listing, showing, and testing BYOK providers.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ from typing import Optional
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from rich.panel import Panel
15
+ from rich.text import Text
16
+
17
+ from ..providers.registry import (
18
+ PROVIDERS,
19
+ ProviderCategory,
20
+ ProviderTier,
21
+ get_providers_by_category,
22
+ get_providers_by_tier,
23
+ get_free_providers,
24
+ get_local_providers,
25
+ )
26
+ from ..providers.gateway import LiteLLMGateway
27
+ from ..providers.local.mlx import get_mlx_client
28
+
29
+
30
+ console = Console()
31
+
32
+
33
+ @click.group()
34
+ def providers():
35
+ """Manage BYOK (Bring Your Own Key) providers."""
36
+ pass
37
+
38
+
39
+ @providers.command("list")
40
+ @click.option(
41
+ "--category",
42
+ type=click.Choice(["us", "china", "other-labs", "model-hosts", "local", "free"]),
43
+ help="Filter by category",
44
+ )
45
+ @click.option(
46
+ "--tier",
47
+ type=click.Choice(["1", "2", "local"]),
48
+ help="Filter by tier",
49
+ )
50
+ @click.option(
51
+ "--configured",
52
+ is_flag=True,
53
+ help="Show only configured providers",
54
+ )
55
+ def list_providers(category: Optional[str], tier: Optional[str], configured: bool):
56
+ """List available BYOK providers."""
57
+
58
+ # Filter providers
59
+ filtered = dict(PROVIDERS)
60
+
61
+ if category:
62
+ category_map = {
63
+ "us": ProviderCategory.US_LABS,
64
+ "china": ProviderCategory.CHINA_LABS,
65
+ "other-labs": ProviderCategory.OTHER_LABS,
66
+ "model-hosts": ProviderCategory.MODEL_HOSTS,
67
+ "local": ProviderCategory.LOCAL,
68
+ }
69
+ if category == "free":
70
+ # Special case: show providers that have free models configured
71
+ filtered = {k: v for k, v in filtered.items() if v.free_models}
72
+ else:
73
+ cat = category_map.get(category)
74
+ if cat:
75
+ filtered = {k: v for k, v in filtered.items() if v.category == cat}
76
+
77
+ if tier:
78
+ tier_map = {
79
+ "1": ProviderTier.TIER1,
80
+ "2": ProviderTier.TIER2,
81
+ "free": ProviderTier.FREE,
82
+ "local": ProviderTier.LOCAL,
83
+ }
84
+ t = tier_map.get(tier)
85
+ if t:
86
+ filtered = {k: v for k, v in filtered.items() if v.tier == t}
87
+
88
+ # Check configuration status
89
+ provider_status = {}
90
+ for provider_id, provider_def in filtered.items():
91
+ is_configured = False
92
+
93
+ if not provider_def.env_vars:
94
+ # Local provider - check if base URL is accessible
95
+ is_configured = True # Assume local is available
96
+ else:
97
+ # Check if any env var is set
98
+ for env_var in provider_def.env_vars:
99
+ if os.environ.get(env_var):
100
+ is_configured = True
101
+ break
102
+
103
+ provider_status[provider_id] = is_configured
104
+
105
+ if configured:
106
+ filtered = {k: v for k, v in filtered.items() if provider_status.get(k)}
107
+
108
+ # Build table
109
+ table = Table(title="BYOK Providers", show_header=True, header_style="bold cyan")
110
+ table.add_column("Provider", style="white")
111
+ table.add_column("Name", style="white")
112
+ table.add_column("Tier", style="dim")
113
+ table.add_column("Category", style="dim")
114
+ table.add_column("Status", style="white")
115
+ table.add_column("Env Var", style="dim")
116
+
117
+ # Sort by category then tier
118
+ sorted_providers = sorted(
119
+ filtered.items(), key=lambda x: (x[1].category.value, x[1].tier.value, x[0])
120
+ )
121
+
122
+ for provider_id, provider_def in sorted_providers:
123
+ is_configured = provider_status.get(provider_id, False)
124
+
125
+ status = "[green]✅ Configured[/green]" if is_configured else "[red]❌ Not configured[/red]"
126
+ if provider_def.category == ProviderCategory.LOCAL and not provider_def.env_vars:
127
+ status = "[blue]🏠 Local[/blue]"
128
+
129
+ env_var = provider_def.env_vars[0] if provider_def.env_vars else "(none)"
130
+
131
+ tier_str = {
132
+ ProviderTier.TIER1: "Tier 1",
133
+ ProviderTier.TIER2: "Tier 2",
134
+ ProviderTier.FREE: "Free",
135
+ ProviderTier.LOCAL: "Local",
136
+ }.get(provider_def.tier, "")
137
+
138
+ table.add_row(
139
+ provider_id,
140
+ provider_def.name,
141
+ tier_str,
142
+ provider_def.category.value,
143
+ status,
144
+ env_var,
145
+ )
146
+
147
+ console.print(table)
148
+
149
+ # Summary
150
+ configured_count = sum(1 for v in provider_status.values() if v)
151
+ console.print(f"\n[dim]Total: {len(filtered)} providers, {configured_count} configured[/dim]")
152
+
153
+
154
+ @providers.command("show")
155
+ @click.argument("provider_id")
156
+ def show_provider(provider_id: str):
157
+ """Show details for a specific provider."""
158
+
159
+ provider_def = PROVIDERS.get(provider_id)
160
+
161
+ if not provider_def:
162
+ console.print(f"[red]Error: Provider '{provider_id}' not found[/red]")
163
+ console.print("\nAvailable providers:")
164
+ for pid in sorted(PROVIDERS.keys()):
165
+ console.print(f" • {pid}")
166
+ return
167
+
168
+ # Check configuration status
169
+ is_configured = False
170
+ configured_env = None
171
+
172
+ for env_var in provider_def.env_vars:
173
+ if os.environ.get(env_var):
174
+ is_configured = True
175
+ configured_env = env_var
176
+ break
177
+
178
+ # Build info panel
179
+ tier_str = {
180
+ ProviderTier.TIER1: "Tier 1 (First-class support)",
181
+ ProviderTier.TIER2: "Tier 2 (Supported)",
182
+ ProviderTier.FREE: "Free Tier",
183
+ ProviderTier.LOCAL: "Local (Self-hosted)",
184
+ }.get(provider_def.tier, "")
185
+
186
+ status = "[green]✅ Configured[/green]" if is_configured else "[red]❌ Not configured[/red]"
187
+ if provider_def.category == ProviderCategory.LOCAL and not provider_def.env_vars:
188
+ status = "[blue]🏠 Local (no API key needed)[/blue]"
189
+
190
+ info_lines = [
191
+ f"[bold]Provider:[/bold] {provider_def.name}",
192
+ f"[bold]ID:[/bold] {provider_id}",
193
+ f"[bold]Tier:[/bold] {tier_str}",
194
+ f"[bold]Category:[/bold] {provider_def.category.value}",
195
+ f"[bold]Status:[/bold] {status}",
196
+ "",
197
+ ]
198
+
199
+ # Environment variables
200
+ if provider_def.env_vars:
201
+ info_lines.append("[bold]Environment Variables:[/bold]")
202
+ for env_var in provider_def.env_vars:
203
+ is_set = bool(os.environ.get(env_var))
204
+ status_icon = "[green]✓[/green]" if is_set else "[red]✗[/red]"
205
+ info_lines.append(f" {status_icon} {env_var}")
206
+ info_lines.append("")
207
+
208
+ if provider_def.optional_env:
209
+ info_lines.append("[bold]Optional Environment Variables:[/bold]")
210
+ for env_var in provider_def.optional_env:
211
+ is_set = bool(os.environ.get(env_var))
212
+ status_icon = "[green]✓[/green]" if is_set else "[dim]○[/dim]"
213
+ info_lines.append(f" {status_icon} {env_var}")
214
+ info_lines.append("")
215
+
216
+ # Base URL
217
+ if provider_def.base_url_env:
218
+ base_url = os.environ.get(
219
+ provider_def.base_url_env, provider_def.default_base_url or "(not set)"
220
+ )
221
+ info_lines.append(f"[bold]Base URL:[/bold] {base_url}")
222
+ info_lines.append(f"[bold]Base URL Env:[/bold] {provider_def.base_url_env}")
223
+ info_lines.append("")
224
+
225
+ # Example models
226
+ if provider_def.example_models:
227
+ info_lines.append("[bold]Example Models:[/bold]")
228
+ for model in provider_def.example_models[:8]:
229
+ info_lines.append(f" • {model}")
230
+ if len(provider_def.example_models) > 8:
231
+ info_lines.append(f" [dim]... and {len(provider_def.example_models) - 8} more[/dim]")
232
+ info_lines.append("")
233
+
234
+ # Free models
235
+ if provider_def.free_models:
236
+ info_lines.append("[bold]Free Models:[/bold]")
237
+ for model in provider_def.free_models:
238
+ info_lines.append(f" • {model}")
239
+ info_lines.append("")
240
+
241
+ # Notes
242
+ if provider_def.notes:
243
+ info_lines.append(f"[bold]Notes:[/bold] {provider_def.notes}")
244
+ info_lines.append("")
245
+
246
+ # Docs
247
+ info_lines.append(f"[bold]Documentation:[/bold] {provider_def.docs_url}")
248
+
249
+ panel = Panel(
250
+ "\n".join(info_lines),
251
+ title=f"Provider: {provider_def.name}",
252
+ border_style="cyan",
253
+ )
254
+ console.print(panel)
255
+
256
+ # Setup instructions if not configured
257
+ if not is_configured and provider_def.env_vars:
258
+ console.print("\n[yellow]To configure this provider:[/yellow]")
259
+ env_var = provider_def.env_vars[0]
260
+ console.print(f' export {env_var}="your-api-key"')
261
+ console.print(f"\n Get your API key at: {provider_def.docs_url}")
262
+
263
+
264
+ @providers.command("test")
265
+ @click.argument("provider_id")
266
+ @click.option("--model", "-m", help="Model to test with")
267
+ def test_provider(provider_id: str, model: Optional[str]):
268
+ """Test connection to a provider."""
269
+
270
+ provider_def = PROVIDERS.get(provider_id)
271
+
272
+ if not provider_def:
273
+ console.print(f"[red]Error: Provider '{provider_id}' not found[/red]")
274
+ return
275
+
276
+ # Check if configured
277
+ is_configured = False
278
+ for env_var in provider_def.env_vars:
279
+ if os.environ.get(env_var):
280
+ is_configured = True
281
+ break
282
+
283
+ if not is_configured and provider_def.env_vars:
284
+ console.print(f"[red]Error: Provider '{provider_id}' is not configured[/red]")
285
+ console.print(f"\nSet one of: {', '.join(provider_def.env_vars)}")
286
+ console.print(f"Get your API key at: {provider_def.docs_url}")
287
+ return
288
+
289
+ # Use first example model if not specified
290
+ test_model = model or (provider_def.example_models[0] if provider_def.example_models else None)
291
+
292
+ if not test_model:
293
+ console.print("[red]Error: No model specified and no example models available[/red]")
294
+ return
295
+
296
+ console.print(f"Testing {provider_def.name} with model {test_model}...")
297
+
298
+ async def run_test():
299
+ gateway = LiteLLMGateway()
300
+ return await gateway.test_connection(provider_id, test_model)
301
+
302
+ try:
303
+ result = asyncio.run(run_test())
304
+
305
+ if result["success"]:
306
+ console.print(f"\n[green]✅ Success![/green]")
307
+ console.print(f" Provider: {result['provider']}")
308
+ console.print(f" Model: {result.get('response_model', test_model)}")
309
+ if result.get("usage"):
310
+ console.print(f" Tokens used: {result['usage'].get('total_tokens', 'N/A')}")
311
+ else:
312
+ console.print(f"\n[red]❌ Failed[/red]")
313
+ console.print(f" Error: {result.get('error', 'Unknown error')}")
314
+ if result.get("error_type"):
315
+ console.print(f" Type: {result['error_type']}")
316
+
317
+ except Exception as e:
318
+ console.print(f"\n[red]❌ Error: {e}[/red]")
319
+
320
+
321
+ @providers.command("mlx")
322
+ @click.argument("action", type=click.Choice(["list", "server", "models", "check", "setup"]))
323
+ @click.option("--model", "-m", help="Model for server command")
324
+ @click.option("--host", default="localhost", help="Server host")
325
+ @click.option("--port", default=8080, type=int, help="Server port")
326
+ def mlx_command(action: str, model: Optional[str], host: str, port: int):
327
+ """Manage MLX (Apple Silicon) models and servers.
328
+
329
+ Actions:
330
+ - list: List available MLX models (cached and server)
331
+ - server: Show command to start MLX server
332
+ - models: Show suggested MLX models
333
+ - check: Check if mlx_lm is installed
334
+ - setup: Complete setup guide for MLX
335
+ """
336
+ from ..providers.local.mlx import MLXClient
337
+
338
+ if action == "list":
339
+ console.print("[bold]🔍 Discovering MLX models...[/bold]")
340
+
341
+ server_running = False
342
+ server_models = []
343
+
344
+ # Show server models if available
345
+ async def check_server():
346
+ nonlocal server_running, server_models
347
+ try:
348
+ client = await get_mlx_client()
349
+ if client:
350
+ console.print("\n[green]🟢 MLX server running:[/green]")
351
+ models = await client.list_models()
352
+ server_running = True
353
+ server_models = models
354
+ if models:
355
+ for model in models:
356
+ console.print(f" • {model.id} ({model.name})")
357
+ else:
358
+ console.print(" No models loaded")
359
+ else:
360
+ console.print("\n[yellow]🟡 MLX server not running[/yellow]")
361
+ except Exception as e:
362
+ console.print(f"\n[red]❌ Error checking server: {e}[/red]")
363
+
364
+ asyncio.run(check_server())
365
+
366
+ # Show cached models (only supported ones)
367
+ console.print("\n[blue]📦 Supported models in HuggingFace cache:[/blue]")
368
+ cache_models = MLXClient.discover_huggingface_models()
369
+ supported_cache_models = [m for m in cache_models if MLXClient.is_model_supported(m["id"])]
370
+
371
+ if supported_cache_models:
372
+ for model in supported_cache_models:
373
+ size_mb = model["size_bytes"] / (1024 * 1024)
374
+ format_note = ""
375
+ if "mlx" in model["id"].lower():
376
+ format_note = " (MLX format)"
377
+ elif "4bit" in model["id"].lower() or "8bit" in model["id"].lower():
378
+ format_note = " (quantized)"
379
+ console.print(f" • {model['id']} ({size_mb:.1f} MB){format_note}")
380
+ else:
381
+ console.print(" No supported MLX models found in cache")
382
+
383
+ # Show guidance if no server running
384
+ if not server_running:
385
+ console.print(
386
+ "\n[green]✅ Supported formats:[/green] MLX (.npz), safetensors (auto-converted)"
387
+ )
388
+ console.print(
389
+ " [green]✅ Working architectures:[/green] Standard transformers, QWen, Llama, Mistral, Phi"
390
+ )
391
+ console.print(
392
+ " [red]❌ Known issues:[/red] MoE models (Mixtral, some gpt-oss) not supported"
393
+ )
394
+
395
+ if supported_cache_models:
396
+ console.print("\n [green]📦 You have supported models available![/green]")
397
+ console.print(" To start MLX server:")
398
+ console.print(
399
+ " 1. [cyan]superqode providers mlx models[/cyan] - See your cached models"
400
+ )
401
+ console.print(
402
+ " 2. [cyan]superqode providers mlx server --model <model-id>[/cyan] - Start server"
403
+ )
404
+ console.print(" 3. [cyan]superqode connect byok mlx <model-id>[/cyan] - Connect")
405
+ else:
406
+ console.print("\n [yellow]📥 No supported models found in cache[/yellow]")
407
+ console.print(" To get started with MLX:")
408
+ console.print(
409
+ " 1. [cyan]superqode providers mlx setup[/cyan] - Complete setup guide"
410
+ )
411
+ console.print(
412
+ " 2. [cyan]mlx_lm.download mlx-community/Llama-3.2-1B-Instruct-4bit[/cyan] - Download model"
413
+ )
414
+ console.print(
415
+ " 3. [cyan]mlx_lm.server --model mlx-community/Llama-3.2-1B-Instruct-4bit[/cyan] - Start server"
416
+ )
417
+ console.print(
418
+ " 4. [cyan]superqode connect byok mlx mlx-community/Llama-3.2-1B-Instruct-4bit[/cyan] - Connect"
419
+ )
420
+
421
+ elif action == "server":
422
+ if not model:
423
+ console.print("[red]❌ Model required for server command[/red]")
424
+ console.print("Usage: superqode providers mlx server --model <model-id>")
425
+ console.print()
426
+ console.print("[yellow]💡 Get model IDs with:[/yellow]")
427
+ console.print(" [cyan]superqode providers mlx models[/cyan]")
428
+ console.print(" [cyan]superqode providers mlx list[/cyan]")
429
+ return
430
+
431
+ console.print(f"[bold]🚀 MLX Server Setup for {model}:[/bold]")
432
+ console.print()
433
+
434
+ from ..providers.local.mlx import MLXClient
435
+
436
+ cmd_parts = MLXClient.get_server_command(model, host, port)
437
+ cmd_str = " ".join(cmd_parts)
438
+
439
+ console.print("[bold]1. Start the MLX server:[/bold]")
440
+ console.print(f" [cyan]{cmd_str}[/cyan]")
441
+ console.print()
442
+ console.print("[bold]2. In another terminal, verify server is running:[/bold]")
443
+ console.print(f" [cyan]curl http://localhost:{port}/v1/models[/cyan]")
444
+ console.print()
445
+ console.print("[bold]3. Connect in SuperQode:[/bold]")
446
+ console.print(f" [cyan]superqode connect byok mlx {model}[/cyan]")
447
+ console.print()
448
+ console.print("[yellow]💡 Pro tips:[/yellow]")
449
+ console.print(" • Large models (20B+) may take 1-2 minutes to load")
450
+ console.print(" • Keep the server terminal open while using MLX models")
451
+ console.print(" • Use Ctrl+C to stop the server when done")
452
+
453
+ elif action == "models":
454
+ console.print("[bold]💡 Suggested MLX Models:[/bold]")
455
+ console.print(" (Optimized for Apple Silicon - fast inference, low memory usage)")
456
+ console.print()
457
+
458
+ models = MLXClient.suggest_models()
459
+ for i, model_id in enumerate(models, 1):
460
+ console.print(f" {i}. [cyan]{model_id}[/cyan]")
461
+
462
+ console.print()
463
+ console.print("[bold]Quick Start Commands:[/bold]")
464
+ console.print(f" [cyan]mlx_lm.download {models[0]}[/cyan] # Download first model")
465
+ console.print(f" [cyan]mlx_lm.server --model {models[0]}[/cyan] # Start server")
466
+ console.print(f" [cyan]superqode connect byok mlx {models[0]}[/cyan] # Connect")
467
+ console.print()
468
+ console.print("[yellow]💡 Model Recommendations:[/yellow]")
469
+ console.print(" • Start with smaller models for testing (1B-3B parameters)")
470
+ console.print(" • Use 4bit/8bit quantized models for best performance")
471
+ console.print(" • Larger models need more RAM but provide better quality")
472
+ console.print(
473
+ " • [red]⚠️ MLX Limitation:[/red] Only one active request per server instance"
474
+ )
475
+
476
+ elif action == "check":
477
+
478
+ async def check_install():
479
+ installed = await MLXClient.check_mlx_lm_installed()
480
+ if installed:
481
+ console.print("[green]✅ mlx_lm is installed[/green]")
482
+ console.print("Version info:")
483
+ # Try to get version
484
+ import subprocess
485
+
486
+ try:
487
+ result = subprocess.run(
488
+ ["mlx_lm.server", "--version"], capture_output=True, text=True, timeout=5
489
+ )
490
+ if result.returncode == 0:
491
+ console.print(f" {result.stdout.strip()}")
492
+ else:
493
+ console.print(" Version check failed")
494
+ except Exception:
495
+ console.print(" Version check failed")
496
+ else:
497
+ console.print("[red]❌ mlx_lm is not installed[/red]")
498
+ console.print()
499
+ console.print("Install with:")
500
+ console.print(" pip install mlx-lm")
501
+ console.print()
502
+ console.print("Or for optional dependency:")
503
+ console.print(" pip install superqode[mlx]")
504
+
505
+ asyncio.run(check_install())
506
+
507
+ elif action == "setup":
508
+ console.print("[bold]🚀 Complete MLX Setup Guide[/bold]")
509
+ console.print()
510
+
511
+ # Supported formats info
512
+ console.print("[green]✅ Supported Formats & Architectures:[/green]")
513
+ console.print(" • [cyan]Formats:[/cyan] MLX (.npz), safetensors (auto-converted)")
514
+ console.print(
515
+ " • [cyan]Architectures:[/cyan] Standard transformers, QWen, Llama, Mistral, Phi"
516
+ )
517
+ console.print(" • [red]Not supported:[/red] MoE models (Mixtral, some gpt-oss variants)")
518
+ console.print()
519
+
520
+ # LM Studio section
521
+ console.print("[bold]🖥️ Alternative: LM Studio (GUI Interface)[/bold]")
522
+ console.print("LM Studio provides a user-friendly GUI for running models locally.")
523
+ console.print()
524
+ console.print("[bold]LM Studio Setup:[/bold]")
525
+ console.print(" 1. [cyan]Download LM Studio:[/cyan] https://lmstudio.ai/")
526
+ console.print(" 2. [cyan]Install and open LM Studio[/cyan]")
527
+ console.print(
528
+ " 3. [cyan]Download a model:[/cyan] Search for models like 'qwen3-30b' or 'llama3.2-3b'"
529
+ )
530
+ console.print(" 4. [cyan]Load the model:[/cyan] Click 'Load Model' in LM Studio")
531
+ console.print(
532
+ " 5. [cyan]Start local server:[/cyan] Go to 'Local Server' tab and click 'Start Server'"
533
+ )
534
+ console.print(" • Default port: 1234")
535
+ console.print(" • Keep LM Studio running in background")
536
+ console.print(" 6. [cyan]Connect in SuperQode:[/cyan] superqode connect byok lmstudio")
537
+ console.print()
538
+ console.print("[yellow]💡 LM Studio Tips:[/yellow]")
539
+ console.print(" • No command-line installation needed")
540
+ console.print(" • GUI shows model loading progress")
541
+ console.print(" • Can test models directly in LM Studio first")
542
+ console.print(" • Server runs on http://localhost:1234/v1/chat/completions")
543
+ console.print()
544
+
545
+ # Check installation
546
+ console.print("[bold]1. Install MLX:[/bold]")
547
+
548
+ async def check_and_guide():
549
+ installed = await MLXClient.check_mlx_lm_installed()
550
+ if installed:
551
+ console.print(" [green]✅ mlx_lm is already installed[/green]")
552
+ else:
553
+ console.print(" [yellow]Install MLX framework:[/yellow]")
554
+ console.print(" [cyan]pip install mlx-lm[/cyan]")
555
+ console.print(" [cyan]# or: pip install superqode[mlx][/cyan]")
556
+ console.print()
557
+
558
+ asyncio.run(check_and_guide())
559
+
560
+ # Show models
561
+ console.print("[bold]2. Choose and Download a Model:[/bold]")
562
+ models = MLXClient.suggest_models()
563
+ console.print(" [yellow]✅ Recommended working models (smallest to largest):[/yellow]")
564
+ for i, model_id in enumerate(models[:6], 1): # Show first 6
565
+ size_indicator = ""
566
+ if "0.6b" in model_id or "1B" in model_id:
567
+ size_indicator = " [green](fast)[/green]"
568
+ elif "3B" in model_id or "7B" in model_id:
569
+ size_indicator = " [yellow](medium)[/yellow]"
570
+ elif "30B" in model_id:
571
+ size_indicator = " [red](large)[/red]"
572
+ console.print(f" {i}. [cyan]{model_id}[/cyan]{size_indicator}")
573
+ console.print()
574
+ console.print(" [yellow]Download a model:[/yellow]")
575
+ console.print(f" [cyan]mlx_lm.download {models[0]}[/cyan] # ~1-2 minutes (small model)")
576
+ console.print(f" [cyan]mlx_lm.download {models[3]}[/cyan] # ~3-5 minutes (medium model)")
577
+ console.print()
578
+ console.print(" [yellow]💡 MLX Limitations:[/yellow]")
579
+ console.print(" • Each model needs its own server instance")
580
+ console.print(" • One server per model for concurrent use")
581
+ console.print(" • Different ports if running multiple servers")
582
+ console.print(" • Only one active request per server instance")
583
+ console.print()
584
+
585
+ # Start server
586
+ console.print("[bold]3. Start the MLX Server:[/bold]")
587
+ console.print(" [yellow]In a separate terminal, run:[/yellow]")
588
+ console.print(f" [cyan]mlx_lm.server --model {models[0]}[/cyan]")
589
+ console.print()
590
+ console.print(" [yellow]Verify server is running:[/yellow]")
591
+ console.print(" [cyan]curl http://localhost:8080/v1/models[/cyan]")
592
+ console.print()
593
+ console.print(
594
+ " [yellow]⚠️ Important:[/yellow] MLX servers handle only ONE request at a time"
595
+ )
596
+ console.print(" • Keep this terminal open while using the model")
597
+ console.print(" • Start separate servers on different ports for concurrent use")
598
+ console.print()
599
+
600
+ # Connect
601
+ console.print("[bold]4. Connect in SuperQode:[/bold]")
602
+ console.print(" [yellow]Open SuperQode and connect:[/yellow]")
603
+ console.print(f" [cyan]superqode connect byok mlx {models[0]}[/cyan]")
604
+ console.print()
605
+
606
+ # Troubleshooting
607
+ console.print("[bold]5. Troubleshooting:[/bold]")
608
+ console.print(" [yellow]If connection fails:[/yellow]")
609
+ console.print(" • Check server is still running in the terminal")
610
+ console.print(" • Large models may take 1-2 minutes to load")
611
+ console.print(" • Try a smaller model first for testing")
612
+ console.print(" • Check RAM usage - MLX needs available memory")
613
+ console.print()
614
+ console.print(" [yellow]Useful commands:[/yellow]")
615
+ console.print(" • [cyan]superqode providers mlx list[/cyan] - See available models")
616
+ console.print(" • [cyan]superqode providers mlx check[/cyan] - Verify installation")
617
+ console.print(" • [cyan]superqode providers mlx models[/cyan] - See all suggestions")
618
+
619
+
620
+ def connect_provider(provider: Optional[str] = None, model: Optional[str] = None) -> int:
621
+ """Connect to a BYOK provider/model via CLI.
622
+
623
+ Args:
624
+ provider: Optional provider ID (e.g., 'ollama', 'anthropic')
625
+ model: Optional model ID (e.g., 'llama3.2', 'claude-3-5-sonnet')
626
+
627
+ Returns:
628
+ Exit code (0 for success, 1 for failure)
629
+ """
630
+ from ..dialogs.provider import ConnectDialog
631
+ from ..providers.manager import ProviderManager
632
+
633
+ manager = ProviderManager()
634
+
635
+ # If both provider and model are provided, try direct connection
636
+ if provider and model:
637
+ # Validate provider exists
638
+ if provider not in PROVIDERS:
639
+ console.print(f"[red]❌ Provider '{provider}' not found.[/red]")
640
+ console.print(f"[dim]Use 'superqode providers list' to see available providers.[/dim]")
641
+ return 1
642
+
643
+ # Test connection
644
+ console.print(f"[cyan]🔍 Testing connection to {provider}/{model}...[/cyan]")
645
+ success, error = manager.test_connection(provider)
646
+
647
+ if not success:
648
+ console.print(f"[red]❌ Connection failed: {error}[/red]")
649
+ provider_def = PROVIDERS[provider]
650
+ if provider_def.env_vars:
651
+ env_var = provider_def.env_vars[0]
652
+ console.print(f"\n[yellow]💡 Set API key:[/yellow]")
653
+ console.print(f"[dim] export {env_var}=your-key[/dim]")
654
+ return 1
655
+
656
+ console.print(f"[green]✓ Connected to {provider}/{model}[/green]")
657
+ console.print(
658
+ "[dim]Note: This is a CLI connection test. For interactive use, run 'superqode' (TUI).[/dim]"
659
+ )
660
+ return 0
661
+
662
+ # If only provider is provided, show model selection dialog
663
+ if provider:
664
+ if provider not in PROVIDERS:
665
+ console.print(f"[red]❌ Provider '{provider}' not found.[/red]")
666
+ console.print(f"[dim]Use 'superqode providers list' to see available providers.[/dim]")
667
+ return 1
668
+
669
+ from ..dialogs.model import ModelDialog
670
+
671
+ dialog = ModelDialog(provider, manager)
672
+ model_id = dialog.show()
673
+
674
+ if model_id:
675
+ console.print(f"[green]✓ Selected: {provider}/{model_id}[/green]")
676
+ console.print(
677
+ "[dim]Note: This is a CLI selection. For interactive use, run 'superqode' (TUI).[/dim]"
678
+ )
679
+ return 0
680
+ else:
681
+ console.print("[yellow]Connection cancelled.[/yellow]")
682
+ return 1
683
+
684
+ # If no provider specified, show full connect dialog
685
+ dialog = ConnectDialog(manager)
686
+ result = dialog.show()
687
+
688
+ if result:
689
+ provider_id, model_id = result
690
+ console.print(f"[green]✓ Connected to {provider_id}/{model_id}[/green]")
691
+ console.print(
692
+ "[dim]Note: This is a CLI selection. For interactive use, run 'superqode' (TUI).[/dim]"
693
+ )
694
+ return 0
695
+ else:
696
+ console.print("[yellow]Connection cancelled.[/yellow]")
697
+ return 1
698
+
699
+
700
+ def connect_local_provider(provider: Optional[str] = None, model: Optional[str] = None) -> int:
701
+ """Connect to a local/self-hosted provider/model via CLI.
702
+
703
+ Args:
704
+ provider: Optional provider ID (e.g., 'ollama', 'lmstudio', 'mlx')
705
+ model: Optional model ID (e.g., 'llama3.2', 'qwen3-30b')
706
+
707
+ Returns:
708
+ Exit code (0 for success, 1 for failure)
709
+ """
710
+ from ..providers.manager import ProviderManager
711
+ from ..providers.registry import get_local_providers, ProviderCategory
712
+
713
+ manager = ProviderManager()
714
+ local_providers = get_local_providers()
715
+
716
+ # If both provider and model are provided, try direct connection
717
+ if provider and model:
718
+ # Validate provider exists and is local
719
+ if provider not in PROVIDERS:
720
+ console.print(f"[red]❌ Provider '{provider}' not found.[/red]")
721
+ console.print(f"[dim]Use 'superqode providers list' to see available providers.[/dim]")
722
+ return 1
723
+
724
+ provider_def = PROVIDERS[provider]
725
+ if provider_def.category != ProviderCategory.LOCAL:
726
+ console.print(f"[red]❌ Provider '{provider}' is not a local provider.[/red]")
727
+ console.print(
728
+ f"[dim]Use 'superqode connect byok {provider}/{model}' for cloud providers.[/dim]"
729
+ )
730
+ console.print(
731
+ f"[dim]Use 'superqode connect local' to see available local providers.[/dim]"
732
+ )
733
+ return 1
734
+
735
+ # Test connection (local providers don't need API keys)
736
+ console.print(f"[cyan]🔍 Testing connection to local {provider}/{model}...[/cyan]")
737
+ success, error = manager.test_connection(provider)
738
+
739
+ if not success:
740
+ console.print(f"[red]❌ Connection failed: {error}[/red]")
741
+ if provider == "ollama":
742
+ console.print(f"\n[yellow]💡 Make sure Ollama is running:[/yellow]")
743
+ console.print(f"[dim] 1. Start Ollama: ollama serve[/dim]")
744
+ console.print(f"[dim] 2. Verify model: ollama list | grep {model}[/dim]")
745
+ console.print(f"[dim] 3. Pull if needed: ollama pull {model}[/dim]")
746
+ elif provider == "mlx":
747
+ console.print(f"\n[yellow]💡 MLX requires a running server:[/yellow]")
748
+ console.print(f"[dim] Run: superqode providers mlx server --model {model}[/dim]")
749
+ elif provider == "lmstudio":
750
+ console.print(f"\n[yellow]💡 LM Studio requires the GUI application:[/yellow]")
751
+ console.print(f"[dim] 1. Download: https://lmstudio.ai/[/dim]")
752
+ console.print(f"[dim] 2. Load model in LM Studio[/dim]")
753
+ console.print(f"[dim] 3. Start Local Server[/dim]")
754
+ return 1
755
+
756
+ console.print(f"[green]✓ Connected to local {provider}/{model}[/green]")
757
+ console.print(
758
+ "[dim]Note: This is a CLI connection test. For interactive use, run 'superqode' (TUI).[/dim]"
759
+ )
760
+ return 0
761
+
762
+ # If only provider is provided, show model selection
763
+ if provider:
764
+ if provider not in PROVIDERS:
765
+ console.print(f"[red]❌ Provider '{provider}' not found.[/red]")
766
+ console.print(f"[dim]Use 'superqode providers list' to see available providers.[/dim]")
767
+ return 1
768
+
769
+ provider_def = PROVIDERS[provider]
770
+ if provider_def.category != ProviderCategory.LOCAL:
771
+ console.print(f"[red]❌ Provider '{provider}' is not a local provider.[/red]")
772
+ console.print(
773
+ f"[dim]Use 'superqode connect byok {provider}' for cloud providers.[/dim]"
774
+ )
775
+ return 1
776
+
777
+ from ..dialogs.model import ModelDialog
778
+
779
+ dialog = ModelDialog(provider, manager)
780
+ model_id = dialog.show()
781
+
782
+ if model_id:
783
+ console.print(f"[green]✓ Selected: {provider}/{model_id}[/green]")
784
+ console.print(
785
+ "[dim]Note: This is a CLI selection. For interactive use, run 'superqode' (TUI).[/dim]"
786
+ )
787
+ return 0
788
+ else:
789
+ console.print("[yellow]Connection cancelled.[/yellow]")
790
+ return 1
791
+
792
+ # If no provider specified, show local providers
793
+ if not local_providers:
794
+ console.print("[yellow]⚠️ No local providers configured.[/yellow]")
795
+ console.print("[dim]Local providers include: ollama, lmstudio, mlx, vllm, etc.[/dim]")
796
+ return 1
797
+
798
+ console.print("\n[bold cyan]💻 Local Providers[/bold cyan]\n")
799
+ console.print("[dim]No API key required - these run on your machine[/dim]\n")
800
+
801
+ for idx, (provider_id, provider_def) in enumerate(local_providers.items(), 1):
802
+ console.print(f" [{idx}] [cyan]{provider_def.name}[/cyan] ({provider_id})")
803
+ console.print(f" {provider_def.description}")
804
+ if provider_def.example_models:
805
+ example = provider_def.example_models[0]
806
+ console.print(
807
+ f" Example: [dim]superqode connect local {provider_id}/{example}[/dim]"
808
+ )
809
+ console.print()
810
+
811
+ console.print("[dim]💡 Use: superqode connect local <provider>[/<model>][/dim]")
812
+ console.print("[dim] Example: superqode connect local ollama/llama3.2[/dim]")
813
+ return 0
814
+
815
+
816
+ # Register with main CLI
817
+ def register_commands(cli):
818
+ """Register provider commands with the main CLI."""
819
+ cli.add_command(providers)