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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
superqode/main.py
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
# Fix CWD before any imports that might resolve it (e.g., logfire via acp, litellm)
|
|
2
|
+
# This prevents FileNotFoundError when current directory doesn't exist
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
cwd = os.getcwd()
|
|
9
|
+
if not pathlib.Path(cwd).exists():
|
|
10
|
+
# Change to home directory if CWD doesn't exist
|
|
11
|
+
os.chdir(os.path.expanduser("~"))
|
|
12
|
+
except (OSError, FileNotFoundError):
|
|
13
|
+
# If getcwd() fails, change to home directory
|
|
14
|
+
try:
|
|
15
|
+
os.chdir(os.path.expanduser("~"))
|
|
16
|
+
except Exception:
|
|
17
|
+
pass # Last resort - let it fail naturally
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Optional, Sequence, Iterable, List, Dict, Any
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
# Global variables for interactive mode
|
|
28
|
+
current_mode: str = "home" # Start in neutral home state
|
|
29
|
+
interactive_modes: dict[str, dict[str, object]] = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Session state management
|
|
33
|
+
class SessionContext:
|
|
34
|
+
"""Tracks work context for handoff between agents."""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.session_id = f"session_{int(time.time())}"
|
|
38
|
+
self.created_at = datetime.now()
|
|
39
|
+
self.updated_at = datetime.now()
|
|
40
|
+
self.current_role = None
|
|
41
|
+
self.previous_role = None
|
|
42
|
+
self.work_description = ""
|
|
43
|
+
self.files_modified = []
|
|
44
|
+
self.files_created = []
|
|
45
|
+
self.tasks_completed = []
|
|
46
|
+
self.tasks_pending = []
|
|
47
|
+
self.quality_issues = []
|
|
48
|
+
self.handoff_history = []
|
|
49
|
+
self.metadata = {}
|
|
50
|
+
|
|
51
|
+
def update_work_context(
|
|
52
|
+
self,
|
|
53
|
+
description: str,
|
|
54
|
+
files_modified: List[str] = None,
|
|
55
|
+
files_created: List[str] = None,
|
|
56
|
+
tasks_completed: List[str] = None,
|
|
57
|
+
tasks_pending: List[str] = None,
|
|
58
|
+
):
|
|
59
|
+
"""Update the current work context."""
|
|
60
|
+
self.work_description = description
|
|
61
|
+
self.updated_at = datetime.now()
|
|
62
|
+
|
|
63
|
+
if files_modified:
|
|
64
|
+
self.files_modified.extend(files_modified)
|
|
65
|
+
if files_created:
|
|
66
|
+
self.files_created.extend(files_created)
|
|
67
|
+
if tasks_completed:
|
|
68
|
+
self.tasks_completed.extend(tasks_completed)
|
|
69
|
+
if tasks_pending:
|
|
70
|
+
self.tasks_pending.extend(tasks_pending)
|
|
71
|
+
|
|
72
|
+
def add_quality_issue(self, issue: str, severity: str = "medium"):
|
|
73
|
+
"""Add a quality issue found during review."""
|
|
74
|
+
self.quality_issues.append(
|
|
75
|
+
{
|
|
76
|
+
"issue": issue,
|
|
77
|
+
"severity": severity,
|
|
78
|
+
"timestamp": datetime.now().isoformat(),
|
|
79
|
+
"resolved": False,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def resolve_quality_issue(self, index: int):
|
|
84
|
+
"""Mark a quality issue as resolved."""
|
|
85
|
+
if 0 <= index < len(self.quality_issues):
|
|
86
|
+
self.quality_issues[index]["resolved"] = True
|
|
87
|
+
self.quality_issues[index]["resolved_at"] = datetime.now().isoformat()
|
|
88
|
+
|
|
89
|
+
def record_handoff(self, from_role: str, to_role: str, reason: str = ""):
|
|
90
|
+
"""Record a handoff event in history."""
|
|
91
|
+
self.handoff_history.append(
|
|
92
|
+
{
|
|
93
|
+
"timestamp": datetime.now().isoformat(),
|
|
94
|
+
"from_role": from_role,
|
|
95
|
+
"to_role": to_role,
|
|
96
|
+
"reason": reason,
|
|
97
|
+
"work_description": self.work_description,
|
|
98
|
+
"quality_issues_count": len([i for i in self.quality_issues if not i["resolved"]]),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
self.previous_role = from_role
|
|
102
|
+
self.current_role = to_role
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
105
|
+
"""Serialize to dictionary for storage."""
|
|
106
|
+
return {
|
|
107
|
+
"session_id": self.session_id,
|
|
108
|
+
"created_at": self.created_at.isoformat(),
|
|
109
|
+
"updated_at": self.updated_at.isoformat(),
|
|
110
|
+
"current_role": self.current_role,
|
|
111
|
+
"previous_role": self.previous_role,
|
|
112
|
+
"work_description": self.work_description,
|
|
113
|
+
"files_modified": self.files_modified,
|
|
114
|
+
"files_created": self.files_created,
|
|
115
|
+
"tasks_completed": self.tasks_completed,
|
|
116
|
+
"tasks_pending": self.tasks_pending,
|
|
117
|
+
"quality_issues": self.quality_issues,
|
|
118
|
+
"handoff_history": self.handoff_history,
|
|
119
|
+
"metadata": self.metadata,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SessionContext":
|
|
124
|
+
"""Deserialize from dictionary."""
|
|
125
|
+
context = cls()
|
|
126
|
+
context.session_id = data.get("session_id", f"session_{int(time.time())}")
|
|
127
|
+
context.created_at = (
|
|
128
|
+
datetime.fromisoformat(data["created_at"]) if "created_at" in data else datetime.now()
|
|
129
|
+
)
|
|
130
|
+
context.updated_at = (
|
|
131
|
+
datetime.fromisoformat(data["updated_at"]) if "updated_at" in data else datetime.now()
|
|
132
|
+
)
|
|
133
|
+
context.current_role = data.get("current_role")
|
|
134
|
+
context.previous_role = data.get("previous_role")
|
|
135
|
+
context.work_description = data.get("work_description", "")
|
|
136
|
+
context.files_modified = data.get("files_modified", [])
|
|
137
|
+
context.files_created = data.get("files_created", [])
|
|
138
|
+
context.tasks_completed = data.get("tasks_completed", [])
|
|
139
|
+
context.tasks_pending = data.get("tasks_pending", [])
|
|
140
|
+
context.quality_issues = data.get("quality_issues", [])
|
|
141
|
+
context.handoff_history = data.get("handoff_history", [])
|
|
142
|
+
context.metadata = data.get("metadata", {})
|
|
143
|
+
return context
|
|
144
|
+
|
|
145
|
+
def save_to_file(self, filepath: Path):
|
|
146
|
+
"""Save context to JSON file."""
|
|
147
|
+
with open(filepath, "w") as f:
|
|
148
|
+
json.dump(self.to_dict(), f, indent=2, default=str)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def load_from_file(cls, filepath: Path) -> Optional["SessionContext"]:
|
|
152
|
+
"""Load context from JSON file."""
|
|
153
|
+
try:
|
|
154
|
+
with open(filepath, "r") as f:
|
|
155
|
+
data = json.load(f)
|
|
156
|
+
return cls.from_dict(data)
|
|
157
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class HandoffWorkflow:
|
|
162
|
+
"""Manages workflow transitions between development and QA roles."""
|
|
163
|
+
|
|
164
|
+
def __init__(self):
|
|
165
|
+
self.context_dir = Path.home() / ".superqode" / "sessions"
|
|
166
|
+
self.context_dir.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
|
|
168
|
+
def initiate_handoff(
|
|
169
|
+
self,
|
|
170
|
+
from_role: str,
|
|
171
|
+
to_role: str,
|
|
172
|
+
context: SessionContext,
|
|
173
|
+
reason: str = "",
|
|
174
|
+
additional_context: str = "",
|
|
175
|
+
) -> str:
|
|
176
|
+
"""Initiate a handoff between roles with context preservation."""
|
|
177
|
+
# Record the handoff
|
|
178
|
+
context.record_handoff(from_role, to_role, reason)
|
|
179
|
+
|
|
180
|
+
# Save current context
|
|
181
|
+
context_file = self.context_dir / f"{context.session_id}.json"
|
|
182
|
+
context.save_to_file(context_file)
|
|
183
|
+
|
|
184
|
+
# Generate handoff message
|
|
185
|
+
handoff_message = self._generate_handoff_message(
|
|
186
|
+
from_role, to_role, context, reason, additional_context
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return handoff_message
|
|
190
|
+
|
|
191
|
+
def _generate_handoff_message(
|
|
192
|
+
self,
|
|
193
|
+
from_role: str,
|
|
194
|
+
to_role: str,
|
|
195
|
+
context: SessionContext,
|
|
196
|
+
reason: str,
|
|
197
|
+
additional_context: str,
|
|
198
|
+
) -> str:
|
|
199
|
+
"""Generate a comprehensive handoff message."""
|
|
200
|
+
message_parts = []
|
|
201
|
+
|
|
202
|
+
# Header
|
|
203
|
+
message_parts.append(f"🤝 **Handoff from {from_role} to {to_role}**")
|
|
204
|
+
message_parts.append(f"📅 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
205
|
+
if reason:
|
|
206
|
+
message_parts.append(f"📝 Reason: {reason}")
|
|
207
|
+
message_parts.append("")
|
|
208
|
+
|
|
209
|
+
# Work description
|
|
210
|
+
if context.work_description:
|
|
211
|
+
message_parts.append("📋 **Work Completed:**")
|
|
212
|
+
message_parts.append(f"{context.work_description}")
|
|
213
|
+
message_parts.append("")
|
|
214
|
+
|
|
215
|
+
# Files changed
|
|
216
|
+
if context.files_modified or context.files_created:
|
|
217
|
+
message_parts.append("📁 **Files Involved:**")
|
|
218
|
+
for file in context.files_created:
|
|
219
|
+
message_parts.append(f" 🆕 {file}")
|
|
220
|
+
for file in context.files_modified:
|
|
221
|
+
message_parts.append(f" ✏️ {file}")
|
|
222
|
+
message_parts.append("")
|
|
223
|
+
|
|
224
|
+
# Tasks
|
|
225
|
+
if context.tasks_completed:
|
|
226
|
+
message_parts.append("✅ **Tasks Completed:**")
|
|
227
|
+
for task in context.tasks_completed:
|
|
228
|
+
message_parts.append(f" • {task}")
|
|
229
|
+
message_parts.append("")
|
|
230
|
+
|
|
231
|
+
if context.tasks_pending:
|
|
232
|
+
message_parts.append("⏳ **Tasks Pending:**")
|
|
233
|
+
for task in context.tasks_pending:
|
|
234
|
+
message_parts.append(f" • {task}")
|
|
235
|
+
message_parts.append("")
|
|
236
|
+
|
|
237
|
+
# Quality issues
|
|
238
|
+
unresolved_issues = [i for i in context.quality_issues if not i["resolved"]]
|
|
239
|
+
if unresolved_issues:
|
|
240
|
+
message_parts.append("⚠️ **Quality Issues Found:**")
|
|
241
|
+
severity_emojis = {"low": "🟢", "medium": "🟡", "high": "🔴", "critical": "💥"}
|
|
242
|
+
for i, issue in enumerate(unresolved_issues):
|
|
243
|
+
emoji = severity_emojis.get(issue["severity"], "🟡")
|
|
244
|
+
message_parts.append(f" {emoji} {issue['issue']}")
|
|
245
|
+
message_parts.append("")
|
|
246
|
+
|
|
247
|
+
# Context for recipient
|
|
248
|
+
role_contexts = {
|
|
249
|
+
"dev.fullstack": "Please review the implementation for code quality, security, and best practices.",
|
|
250
|
+
"qa.api_tester": "Please test the functionality, validate requirements, and identify any issues.",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if to_role in role_contexts:
|
|
254
|
+
message_parts.append(f"🎯 **Your Role:** {role_contexts[to_role]}")
|
|
255
|
+
|
|
256
|
+
# Additional context
|
|
257
|
+
if additional_context:
|
|
258
|
+
message_parts.append("")
|
|
259
|
+
message_parts.append("📎 **Additional Context:**")
|
|
260
|
+
message_parts.append(additional_context)
|
|
261
|
+
|
|
262
|
+
return "\n".join(message_parts)
|
|
263
|
+
|
|
264
|
+
def get_pending_handoffs(self) -> List[Dict[str, Any]]:
|
|
265
|
+
"""Get list of pending handoffs that need attention."""
|
|
266
|
+
pending = []
|
|
267
|
+
for context_file in self.context_dir.glob("*.json"):
|
|
268
|
+
context = SessionContext.load_from_file(context_file)
|
|
269
|
+
if context:
|
|
270
|
+
# Show handoffs that are not yet approved
|
|
271
|
+
if not context.metadata.get("approved", False):
|
|
272
|
+
pending.append(
|
|
273
|
+
{
|
|
274
|
+
"session_id": context.session_id,
|
|
275
|
+
"current_role": context.current_role,
|
|
276
|
+
"work_description": context.work_description,
|
|
277
|
+
"pending_tasks": len(context.tasks_pending),
|
|
278
|
+
"quality_issues": len(
|
|
279
|
+
[i for i in context.quality_issues if not i["resolved"]]
|
|
280
|
+
),
|
|
281
|
+
"last_updated": context.updated_at,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
return sorted(pending, key=lambda x: x["last_updated"], reverse=True)
|
|
285
|
+
|
|
286
|
+
def approve_work(self, session_id: str, approval_notes: str = "") -> bool:
|
|
287
|
+
"""Approve work for deployment."""
|
|
288
|
+
context_file = self.context_dir / f"{session_id}.json"
|
|
289
|
+
context = SessionContext.load_from_file(context_file)
|
|
290
|
+
|
|
291
|
+
if not context:
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
# Mark all quality issues as resolved
|
|
295
|
+
for issue in context.quality_issues:
|
|
296
|
+
if not issue["resolved"]:
|
|
297
|
+
issue["resolved"] = True
|
|
298
|
+
issue["resolved_at"] = datetime.now().isoformat()
|
|
299
|
+
issue["approved_by"] = context.current_role
|
|
300
|
+
|
|
301
|
+
# Clear pending tasks
|
|
302
|
+
context.tasks_pending.clear()
|
|
303
|
+
|
|
304
|
+
# Add approval metadata
|
|
305
|
+
context.metadata["approved"] = True
|
|
306
|
+
context.metadata["approved_at"] = datetime.now().isoformat()
|
|
307
|
+
context.metadata["approved_by"] = context.current_role
|
|
308
|
+
context.metadata["approval_notes"] = approval_notes
|
|
309
|
+
|
|
310
|
+
# Save updated context
|
|
311
|
+
context.save_to_file(context_file)
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class SessionState:
|
|
316
|
+
def __init__(self):
|
|
317
|
+
self.state = "superqode" # "superqode" | "agent_connected" | "role_mode"
|
|
318
|
+
self.connected_agent = None # Agent data when in agent_connected state
|
|
319
|
+
self.agent_role_info = None # Role info when connected via role
|
|
320
|
+
self.current_context = SessionContext() # Current work context
|
|
321
|
+
self.handoff_workflow = HandoffWorkflow() # Handoff management
|
|
322
|
+
self.acp_manager = None # ACP agent manager for real connections
|
|
323
|
+
self.execution_mode = "acp" # "acp" or "byok"
|
|
324
|
+
|
|
325
|
+
def connect_to_agent(self, agent_data, role_info=None, model=None, execution_mode="acp"):
|
|
326
|
+
"""Connect to an agent directly (bypassing roles)
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
agent_data: Agent information dict
|
|
330
|
+
role_info: Optional role information
|
|
331
|
+
model: Optional model override
|
|
332
|
+
execution_mode: "acp" for coding agent, "byok" for direct LLM
|
|
333
|
+
"""
|
|
334
|
+
self.state = "agent_connected"
|
|
335
|
+
self.connected_agent = agent_data
|
|
336
|
+
self.agent_role_info = role_info
|
|
337
|
+
self.selected_model = model # Store selected model for direct connections
|
|
338
|
+
self.execution_mode = execution_mode # Track execution mode
|
|
339
|
+
|
|
340
|
+
def set_acp_manager(self, manager):
|
|
341
|
+
"""Set the active ACP manager for real-time communication"""
|
|
342
|
+
self.acp_manager = manager
|
|
343
|
+
|
|
344
|
+
def disconnect_acp_manager(self):
|
|
345
|
+
"""Disconnect the ACP manager"""
|
|
346
|
+
if self.acp_manager:
|
|
347
|
+
import asyncio
|
|
348
|
+
|
|
349
|
+
asyncio.run(self.acp_manager.disconnect())
|
|
350
|
+
self.acp_manager = None
|
|
351
|
+
|
|
352
|
+
def disconnect_agent(self):
|
|
353
|
+
"""Disconnect from agent and return to superqode mode"""
|
|
354
|
+
self.state = "superqode"
|
|
355
|
+
self.connected_agent = None
|
|
356
|
+
self.agent_role_info = None
|
|
357
|
+
self.selected_model = None
|
|
358
|
+
self.execution_mode = "acp" # Reset to default
|
|
359
|
+
|
|
360
|
+
def switch_to_role_mode(self, mode):
|
|
361
|
+
"""Switch to role-based mode"""
|
|
362
|
+
self.state = "role_mode"
|
|
363
|
+
global current_mode
|
|
364
|
+
current_mode = mode
|
|
365
|
+
|
|
366
|
+
# Check for pending handoffs for this role
|
|
367
|
+
pending = self.get_pending_handoffs()
|
|
368
|
+
role_handoffs = [h for h in pending if h["current_role"] == mode]
|
|
369
|
+
|
|
370
|
+
if role_handoffs:
|
|
371
|
+
# Automatically resume the most recent handoff for this role
|
|
372
|
+
latest_handoff = role_handoffs[0] # Already sorted by updated_at desc
|
|
373
|
+
if self.load_context_from_session(latest_handoff["session_id"]):
|
|
374
|
+
print(f"🤝 Resumed pending handoff: {latest_handoff['work_description'][:50]}...")
|
|
375
|
+
return True
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
def is_connected_to_agent(self):
|
|
379
|
+
"""Check if currently connected to an agent"""
|
|
380
|
+
return self.state == "agent_connected" and self.connected_agent is not None
|
|
381
|
+
|
|
382
|
+
def get_prompt_suffix(self):
|
|
383
|
+
"""Get the prompt suffix based on current state"""
|
|
384
|
+
if self.state == "agent_connected":
|
|
385
|
+
agent_name = (
|
|
386
|
+
self.connected_agent.get("short_name", "Unknown")
|
|
387
|
+
if self.connected_agent
|
|
388
|
+
else "Unknown"
|
|
389
|
+
)
|
|
390
|
+
# Show execution mode in prompt
|
|
391
|
+
if self.execution_mode == "acp":
|
|
392
|
+
return f"🔗 ACP • {agent_name.upper()}"
|
|
393
|
+
elif self.execution_mode == "byok":
|
|
394
|
+
return f"⚡ BYOK • {agent_name.upper()}"
|
|
395
|
+
else:
|
|
396
|
+
return f"🔗 {agent_name.upper()}"
|
|
397
|
+
elif self.state == "role_mode":
|
|
398
|
+
return current_mode.replace(".", "/").upper()
|
|
399
|
+
else: # superqode
|
|
400
|
+
if current_mode == "home":
|
|
401
|
+
return "🏠 HOME"
|
|
402
|
+
else:
|
|
403
|
+
return current_mode.replace(".", "/").upper()
|
|
404
|
+
|
|
405
|
+
def get_connection_info(self):
|
|
406
|
+
"""Get detailed connection information for display"""
|
|
407
|
+
if not self.is_connected_to_agent():
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
info = {
|
|
411
|
+
"agent": self.connected_agent.get("name", "Unknown")
|
|
412
|
+
if self.connected_agent
|
|
413
|
+
else "Unknown",
|
|
414
|
+
"short_name": self.connected_agent.get("short_name", "unknown")
|
|
415
|
+
if self.connected_agent
|
|
416
|
+
else "unknown",
|
|
417
|
+
"type": self.connected_agent.get("type", "unknown")
|
|
418
|
+
if self.connected_agent
|
|
419
|
+
else "unknown",
|
|
420
|
+
"description": self.connected_agent.get("description", "")
|
|
421
|
+
if self.connected_agent
|
|
422
|
+
else "",
|
|
423
|
+
"execution_mode": self.execution_mode, # Include execution mode
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# Add role info if connected via role
|
|
427
|
+
if self.agent_role_info:
|
|
428
|
+
info.update(
|
|
429
|
+
{
|
|
430
|
+
"role": self.agent_role_info.get("role", ""),
|
|
431
|
+
"provider": self.agent_role_info.get("provider", ""),
|
|
432
|
+
"model": self.agent_role_info.get("model", ""),
|
|
433
|
+
"job_description": self.agent_role_info.get("job_description", ""),
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return info
|
|
438
|
+
|
|
439
|
+
def update_context(
|
|
440
|
+
self,
|
|
441
|
+
description: str = None,
|
|
442
|
+
files_modified: List[str] = None,
|
|
443
|
+
files_created: List[str] = None,
|
|
444
|
+
tasks_completed: List[str] = None,
|
|
445
|
+
tasks_pending: List[str] = None,
|
|
446
|
+
):
|
|
447
|
+
"""Update the current work context."""
|
|
448
|
+
if description or files_modified or files_created or tasks_completed or tasks_pending:
|
|
449
|
+
self.current_context.update_work_context(
|
|
450
|
+
description or self.current_context.work_description,
|
|
451
|
+
files_modified,
|
|
452
|
+
files_created,
|
|
453
|
+
tasks_completed,
|
|
454
|
+
tasks_pending,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def add_quality_issue(self, issue: str, severity: str = "medium"):
|
|
458
|
+
"""Add a quality issue to the current context."""
|
|
459
|
+
self.current_context.add_quality_issue(issue, severity)
|
|
460
|
+
|
|
461
|
+
def resolve_quality_issue(self, index: int):
|
|
462
|
+
"""Resolve a quality issue by index."""
|
|
463
|
+
self.current_context.resolve_quality_issue(index)
|
|
464
|
+
|
|
465
|
+
def initiate_handoff(self, to_role: str, reason: str = "", additional_context: str = "") -> str:
|
|
466
|
+
"""Initiate a handoff to another role."""
|
|
467
|
+
from_role = self.get_current_role_name()
|
|
468
|
+
|
|
469
|
+
if not from_role:
|
|
470
|
+
return "Error: Not currently in a role mode for handoff"
|
|
471
|
+
|
|
472
|
+
handoff_message = self.handoff_workflow.initiate_handoff(
|
|
473
|
+
from_role, to_role, self.current_context, reason, additional_context
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Reset context for new role (but keep session ID)
|
|
477
|
+
old_session_id = self.current_context.session_id
|
|
478
|
+
self.current_context = SessionContext()
|
|
479
|
+
self.current_context.session_id = old_session_id
|
|
480
|
+
self.current_context.previous_role = from_role
|
|
481
|
+
self.current_context.current_role = to_role
|
|
482
|
+
|
|
483
|
+
return handoff_message
|
|
484
|
+
|
|
485
|
+
def approve_work(self, approval_notes: str = "") -> bool:
|
|
486
|
+
"""Approve current work for deployment."""
|
|
487
|
+
return self.handoff_workflow.approve_work(self.current_context.session_id, approval_notes)
|
|
488
|
+
|
|
489
|
+
def get_pending_handoffs(self) -> List[Dict[str, Any]]:
|
|
490
|
+
"""Get list of pending handoffs."""
|
|
491
|
+
return self.handoff_workflow.get_pending_handoffs()
|
|
492
|
+
|
|
493
|
+
def get_current_role_name(self) -> Optional[str]:
|
|
494
|
+
"""Get the current role name for handoffs."""
|
|
495
|
+
if self.state == "role_mode":
|
|
496
|
+
return current_mode
|
|
497
|
+
elif self.agent_role_info:
|
|
498
|
+
role = self.agent_role_info.get("role", "")
|
|
499
|
+
mode = self.agent_role_info.get("mode", "")
|
|
500
|
+
if mode and role:
|
|
501
|
+
return f"{mode}.{role}"
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
def load_context_from_session(self, session_id: str) -> bool:
|
|
505
|
+
"""Load a previous session context."""
|
|
506
|
+
context_file = self.handoff_workflow.context_dir / f"{session_id}.json"
|
|
507
|
+
context = SessionContext.load_from_file(context_file)
|
|
508
|
+
if context:
|
|
509
|
+
self.current_context = context
|
|
510
|
+
return True
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# Global session state instance
|
|
515
|
+
session = SessionState()
|
|
516
|
+
|
|
517
|
+
# Main CLI group
|
|
518
|
+
import click
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@click.group(invoke_without_command=True)
|
|
522
|
+
@click.version_option(version="0.1.5")
|
|
523
|
+
@click.option("--tui", is_flag=True, help="Launch the Textual TUI interface")
|
|
524
|
+
@click.pass_context
|
|
525
|
+
def cli_main(ctx, tui):
|
|
526
|
+
"""SuperQode - Developer TUI for multi-agent coding and exploration.
|
|
527
|
+
|
|
528
|
+
Interactive interface for orchestrating coding agents across dev, QE, and DevOps.
|
|
529
|
+
For automation and CI, use the `superqe` CLI.
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
# If no command is provided, launch Textual app (default behavior)
|
|
533
|
+
if ctx.invoked_subcommand is None or tui:
|
|
534
|
+
import time
|
|
535
|
+
|
|
536
|
+
# Show simple loading message before TUI starts
|
|
537
|
+
print("🚀 Starting SuperQode...", end="", flush=True)
|
|
538
|
+
time.sleep(0.5)
|
|
539
|
+
|
|
540
|
+
# Clear the loading message before TUI takes over
|
|
541
|
+
print("\r" + " " * 50 + "\r", end="", flush=True)
|
|
542
|
+
|
|
543
|
+
# Import and run the TUI
|
|
544
|
+
from superqode.app import run_textual_app
|
|
545
|
+
|
|
546
|
+
run_textual_app()
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# Configuration management commands - defined before main() for proper registration
|
|
551
|
+
@cli_main.group()
|
|
552
|
+
def config():
|
|
553
|
+
"""Manage SuperQode configuration."""
|
|
554
|
+
pass
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@config.command("list-modes")
|
|
558
|
+
def config_list_modes():
|
|
559
|
+
"""List all configured modes and roles."""
|
|
560
|
+
from superqode.config import load_enabled_modes
|
|
561
|
+
from rich.console import Console
|
|
562
|
+
from rich.table import Table
|
|
563
|
+
|
|
564
|
+
console = Console()
|
|
565
|
+
enabled_modes = load_enabled_modes()
|
|
566
|
+
|
|
567
|
+
if not enabled_modes:
|
|
568
|
+
console.print(
|
|
569
|
+
"[yellow]No modes configured. Run 'superqode init' to create a repo configuration.[/yellow]"
|
|
570
|
+
)
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
table = Table(title="Configured Modes and Roles")
|
|
574
|
+
table.add_column("Mode", style="cyan", no_wrap=True)
|
|
575
|
+
table.add_column("Role", style="magenta", no_wrap=True)
|
|
576
|
+
table.add_column("Agent", style="green")
|
|
577
|
+
table.add_column("Description", style="white")
|
|
578
|
+
|
|
579
|
+
for mode_name, mode_config in enabled_modes.items():
|
|
580
|
+
if mode_config.direct_role:
|
|
581
|
+
table.add_row(
|
|
582
|
+
mode_name,
|
|
583
|
+
"(direct)",
|
|
584
|
+
f"{mode_config.direct_role.coding_agent} ({mode_config.direct_role.agent_type})",
|
|
585
|
+
mode_config.direct_role.description,
|
|
586
|
+
)
|
|
587
|
+
elif mode_config.roles:
|
|
588
|
+
for role_name, role_config in mode_config.roles.items():
|
|
589
|
+
table.add_row(
|
|
590
|
+
mode_name,
|
|
591
|
+
role_name,
|
|
592
|
+
f"{role_config.coding_agent} ({role_config.agent_type})",
|
|
593
|
+
role_config.description,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
console.print(table)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@config.command("init")
|
|
600
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite existing configuration")
|
|
601
|
+
def config_init(force):
|
|
602
|
+
"""Initialize default SuperQode configuration."""
|
|
603
|
+
from superqode.config import create_default_config, save_config, find_config_file
|
|
604
|
+
from pathlib import Path
|
|
605
|
+
import os
|
|
606
|
+
|
|
607
|
+
config_path = find_config_file()
|
|
608
|
+
if config_path and config_path.exists() and not force:
|
|
609
|
+
click.echo(f"Configuration already exists at {config_path}")
|
|
610
|
+
click.echo("Use --force to overwrite")
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
if not config_path:
|
|
614
|
+
config_path = Path.cwd() / "superqode.yaml"
|
|
615
|
+
|
|
616
|
+
# Create default config
|
|
617
|
+
config = create_default_config()
|
|
618
|
+
save_config(config, config_path)
|
|
619
|
+
|
|
620
|
+
click.echo(f"Created default configuration at {config_path}")
|
|
621
|
+
click.echo("Edit the file to customize your development team!")
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@config.command("set-model")
|
|
625
|
+
@click.argument("mode_role", metavar="MODE.ROLE")
|
|
626
|
+
@click.argument("model", metavar="MODEL")
|
|
627
|
+
def config_set_model(mode_role, model):
|
|
628
|
+
"""Set the model for a specific mode/role."""
|
|
629
|
+
from superqode.config import load_config, save_config, resolve_role
|
|
630
|
+
|
|
631
|
+
parts = mode_role.split(".", 1)
|
|
632
|
+
if len(parts) != 2:
|
|
633
|
+
click.echo("Error: MODE.ROLE must be in format 'mode.role' (e.g., 'dev.backend')")
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
mode_name, role_name = parts
|
|
637
|
+
config = load_config()
|
|
638
|
+
|
|
639
|
+
resolved_role = resolve_role(mode_name, role_name, config)
|
|
640
|
+
if not resolved_role:
|
|
641
|
+
click.echo(f"Error: Role '{mode_role}' not found in configuration")
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
if resolved_role.agent_type == "acp":
|
|
645
|
+
click.echo("Error: Cannot set model for ACP agents. ACP agents use their own models.")
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
# Update the configuration
|
|
649
|
+
if role_name:
|
|
650
|
+
config.team.modes[mode_name].roles[role_name].model = model
|
|
651
|
+
else:
|
|
652
|
+
config.team.modes[mode_name].model = model
|
|
653
|
+
|
|
654
|
+
save_config(config)
|
|
655
|
+
click.echo(f"Updated {mode_role} to use model '{model}'")
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@config.command("set-agent")
|
|
659
|
+
@click.argument("mode_role", metavar="MODE.ROLE")
|
|
660
|
+
@click.argument("agent", metavar="AGENT")
|
|
661
|
+
@click.option("--provider", "-p", help="Provider for SuperQode agents")
|
|
662
|
+
def config_set_agent(mode_role, agent, provider):
|
|
663
|
+
"""Set the agent for a specific mode/role."""
|
|
664
|
+
from superqode.config import load_config, save_config, resolve_role
|
|
665
|
+
|
|
666
|
+
parts = mode_role.split(".", 1)
|
|
667
|
+
if len(parts) != 2:
|
|
668
|
+
click.echo("Error: MODE.ROLE must be in format 'mode.role' (e.g., 'dev.backend')")
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
mode_name, role_name = parts
|
|
672
|
+
config = load_config()
|
|
673
|
+
|
|
674
|
+
resolved_role = resolve_role(mode_name, role_name, config)
|
|
675
|
+
if not resolved_role:
|
|
676
|
+
click.echo(f"Error: Role '{mode_role}' not found in configuration")
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
# Update the configuration
|
|
680
|
+
if role_name:
|
|
681
|
+
config.team.modes[mode_name].roles[role_name].coding_agent = agent
|
|
682
|
+
if provider:
|
|
683
|
+
config.team.modes[mode_name].roles[role_name].provider = provider
|
|
684
|
+
else:
|
|
685
|
+
config.team.modes[mode_name].coding_agent = agent
|
|
686
|
+
if provider:
|
|
687
|
+
config.team.modes[mode_name].provider = provider
|
|
688
|
+
|
|
689
|
+
save_config(config)
|
|
690
|
+
click.echo(
|
|
691
|
+
f"Updated {mode_role} to use agent '{agent}'{' with provider ' + provider if provider else ''}"
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@config.command("enable-role")
|
|
696
|
+
@click.argument("mode_role", metavar="MODE.ROLE")
|
|
697
|
+
def config_enable_role(mode_role):
|
|
698
|
+
"""Enable a disabled role."""
|
|
699
|
+
from superqode.config import load_config, save_config
|
|
700
|
+
|
|
701
|
+
parts = mode_role.split(".", 1)
|
|
702
|
+
if len(parts) != 2:
|
|
703
|
+
click.echo("Error: MODE.ROLE must be in format 'mode.role' (e.g., 'dev.mobile')")
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
mode_name, role_name = parts
|
|
707
|
+
config = load_config()
|
|
708
|
+
|
|
709
|
+
if mode_name not in config.team.modes:
|
|
710
|
+
click.echo(f"Error: Mode '{mode_name}' not found")
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
mode_config = config.team.modes[mode_name]
|
|
714
|
+
if role_name not in mode_config.roles:
|
|
715
|
+
click.echo(f"Error: Role '{role_name}' not found in mode '{mode_name}'")
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
mode_config.roles[role_name].enabled = True
|
|
719
|
+
save_config(config)
|
|
720
|
+
click.echo(f"Enabled role '{mode_role}'")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@config.command("disable-role")
|
|
724
|
+
@click.argument("mode_role", metavar="MODE.ROLE")
|
|
725
|
+
def config_disable_role(mode_role):
|
|
726
|
+
"""Disable an enabled role."""
|
|
727
|
+
from superqode.config import load_config, save_config
|
|
728
|
+
|
|
729
|
+
parts = mode_role.split(".", 1)
|
|
730
|
+
if len(parts) != 2:
|
|
731
|
+
click.echo("Error: MODE.ROLE must be in format 'mode.role' (e.g., 'dev.mobile')")
|
|
732
|
+
return
|
|
733
|
+
|
|
734
|
+
mode_name, role_name = parts
|
|
735
|
+
config = load_config()
|
|
736
|
+
|
|
737
|
+
if mode_name not in config.team.modes:
|
|
738
|
+
click.echo(f"Error: Mode '{mode_name}' not found")
|
|
739
|
+
return
|
|
740
|
+
|
|
741
|
+
mode_config = config.team.modes[mode_name]
|
|
742
|
+
if role_name not in mode_config.roles:
|
|
743
|
+
click.echo(f"Error: Role '{role_name}' not found in mode '{mode_name}'")
|
|
744
|
+
return
|
|
745
|
+
|
|
746
|
+
mode_config.roles[role_name].enabled = False
|
|
747
|
+
save_config(config)
|
|
748
|
+
click.echo(f"Disabled role '{mode_role}'")
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# TUI command
|
|
752
|
+
@cli_main.command("tui")
|
|
753
|
+
def tui_command():
|
|
754
|
+
"""Launch the Textual TUI interface."""
|
|
755
|
+
from superqode.app import run_textual_app
|
|
756
|
+
|
|
757
|
+
run_textual_app()
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
# Init command (top-level for convenience)
|
|
761
|
+
@cli_main.command("init")
|
|
762
|
+
@click.option("--force", "-f", is_flag=True, help="Overwrite existing configuration")
|
|
763
|
+
def init_command(force):
|
|
764
|
+
"""Initialize SuperQode in current directory.
|
|
765
|
+
|
|
766
|
+
Creates a superqode.yaml with all team roles enabled
|
|
767
|
+
configured to use OpenCode with free models.
|
|
768
|
+
"""
|
|
769
|
+
from superqode.config import find_config_file
|
|
770
|
+
from pathlib import Path
|
|
771
|
+
import os
|
|
772
|
+
|
|
773
|
+
config_path = find_config_file()
|
|
774
|
+
if config_path and config_path.exists() and not force:
|
|
775
|
+
click.echo(f"✓ Configuration already exists at {config_path}")
|
|
776
|
+
click.echo(" Use --force to overwrite")
|
|
777
|
+
return
|
|
778
|
+
|
|
779
|
+
config_path = Path.cwd() / "superqode.yaml"
|
|
780
|
+
|
|
781
|
+
# Copy the full configuration from the template
|
|
782
|
+
template_path = Path(__file__).parent.parent.parent / "superqode-template.yaml"
|
|
783
|
+
if template_path.exists():
|
|
784
|
+
import shutil
|
|
785
|
+
|
|
786
|
+
shutil.copy2(template_path, config_path)
|
|
787
|
+
click.echo(f"✓ Created {config_path} with all roles available")
|
|
788
|
+
else:
|
|
789
|
+
# Fallback: create basic config if template not found
|
|
790
|
+
default_config = """# =============================================================================
|
|
791
|
+
# SuperQode - Team Configuration
|
|
792
|
+
# =============================================================================
|
|
793
|
+
# Multi-agent software development team
|
|
794
|
+
# Run: superqode (TUI) or superqode --help (CLI)
|
|
795
|
+
# =============================================================================
|
|
796
|
+
|
|
797
|
+
superqode:
|
|
798
|
+
version: "1.0"
|
|
799
|
+
team_name: "Full Stack Development Team"
|
|
800
|
+
description: "AI-powered software development team"
|
|
801
|
+
|
|
802
|
+
# Default configuration for all roles
|
|
803
|
+
default:
|
|
804
|
+
mode: "acp"
|
|
805
|
+
agent: "opencode"
|
|
806
|
+
agent_config:
|
|
807
|
+
provider: "opencode"
|
|
808
|
+
model: "glm-4.7-free"
|
|
809
|
+
|
|
810
|
+
# =============================================================================
|
|
811
|
+
# TEAM ROLES - All enabled by default
|
|
812
|
+
# =============================================================================
|
|
813
|
+
team:
|
|
814
|
+
# Development roles
|
|
815
|
+
dev:
|
|
816
|
+
description: "Software Development"
|
|
817
|
+
roles:
|
|
818
|
+
fullstack:
|
|
819
|
+
description: "Full-stack development"
|
|
820
|
+
mode: "acp"
|
|
821
|
+
agent: "opencode"
|
|
822
|
+
agent_config:
|
|
823
|
+
provider: "opencode"
|
|
824
|
+
model: "glm-4.7-free"
|
|
825
|
+
enabled: false
|
|
826
|
+
job_description: |
|
|
827
|
+
You are a Senior Full-Stack Developer.
|
|
828
|
+
Write clean, maintainable code. Follow best practices.
|
|
829
|
+
Implement features end-to-end across frontend and backend.
|
|
830
|
+
|
|
831
|
+
# QE roles
|
|
832
|
+
qe:
|
|
833
|
+
description: "Quality Engineering"
|
|
834
|
+
roles:
|
|
835
|
+
fullstack:
|
|
836
|
+
description: "Full-stack QE engineer"
|
|
837
|
+
mode: "acp"
|
|
838
|
+
agent: "opencode"
|
|
839
|
+
agent_config:
|
|
840
|
+
provider: "opencode"
|
|
841
|
+
model: "grok-code"
|
|
842
|
+
enabled: false
|
|
843
|
+
job_description: |
|
|
844
|
+
You are a Senior QE Engineer.
|
|
845
|
+
Review code for bugs, security issues, and best practices.
|
|
846
|
+
Write and run tests. Validate requirements are met.
|
|
847
|
+
|
|
848
|
+
# DevOps roles
|
|
849
|
+
devops:
|
|
850
|
+
description: "DevOps & Infrastructure"
|
|
851
|
+
roles:
|
|
852
|
+
fullstack:
|
|
853
|
+
description: "Full-stack DevOps engineer"
|
|
854
|
+
mode: "acp"
|
|
855
|
+
agent: "opencode"
|
|
856
|
+
agent_config:
|
|
857
|
+
provider: "opencode"
|
|
858
|
+
model: "gpt-5-nano"
|
|
859
|
+
enabled: false
|
|
860
|
+
job_description: |
|
|
861
|
+
You are a Senior DevOps Engineer.
|
|
862
|
+
Design CI/CD pipelines, containerize apps, manage infrastructure.
|
|
863
|
+
Ensure security, monitoring, and deployment best practices.
|
|
864
|
+
|
|
865
|
+
# =============================================================================
|
|
866
|
+
# Available free models: glm-4.7-free, grok-code, gpt-5-nano,
|
|
867
|
+
# minimax-m2.1-free, big-pickle
|
|
868
|
+
# =============================================================================
|
|
869
|
+
"""
|
|
870
|
+
|
|
871
|
+
with open(config_path, "w") as f:
|
|
872
|
+
f.write(default_config)
|
|
873
|
+
click.echo(f"✓ Created {config_path} with basic roles available")
|
|
874
|
+
|
|
875
|
+
click.echo("")
|
|
876
|
+
click.echo(" Quick start:")
|
|
877
|
+
click.echo(" superqode # Launch TUI")
|
|
878
|
+
click.echo(" superqe roles # List configured QE roles")
|
|
879
|
+
click.echo(" superqe run . # Run QE using your superqode.yaml")
|
|
880
|
+
click.echo("")
|
|
881
|
+
click.echo(" Edit superqode.yaml to add or enable roles as needed.")
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
# ACP Agent commands
|
|
885
|
+
@cli_main.group()
|
|
886
|
+
def agents():
|
|
887
|
+
"""Manage ACP (Agent-Client Protocol) coding agents."""
|
|
888
|
+
pass
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@agents.command("list")
|
|
892
|
+
@click.option("--store", is_flag=True, help="Show agent store interface")
|
|
893
|
+
def agents_list(store):
|
|
894
|
+
"""List all available ACP coding agents."""
|
|
895
|
+
from superqode.commands.acp import show_agents_list, show_agents_store
|
|
896
|
+
|
|
897
|
+
if store:
|
|
898
|
+
show_agents_store()
|
|
899
|
+
else:
|
|
900
|
+
show_agents_list()
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
@agents.command("store")
|
|
904
|
+
def agents_store():
|
|
905
|
+
"""Show the beautiful agent store interface."""
|
|
906
|
+
from superqode.commands.acp import show_agents_store
|
|
907
|
+
|
|
908
|
+
show_agents_store()
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
@agents.command("show")
|
|
912
|
+
@click.argument("agent", metavar="AGENT")
|
|
913
|
+
def agents_show(agent):
|
|
914
|
+
"""Show detailed information about a specific agent."""
|
|
915
|
+
from superqode.commands.acp import show_agent
|
|
916
|
+
|
|
917
|
+
show_agent(agent)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
@agents.command("connect")
|
|
921
|
+
@click.argument("agent", metavar="AGENT")
|
|
922
|
+
@click.option("--project-dir", "-d", metavar="DIR", help="Project directory to work in")
|
|
923
|
+
def agents_connect(agent, project_dir):
|
|
924
|
+
"""Connect to an ACP coding agent. (Deprecated: use 'superqode connect acp' instead)"""
|
|
925
|
+
import warnings
|
|
926
|
+
|
|
927
|
+
warnings.warn(
|
|
928
|
+
"'superqode agents connect' is deprecated. Use 'superqode connect acp' instead.",
|
|
929
|
+
DeprecationWarning,
|
|
930
|
+
stacklevel=2,
|
|
931
|
+
)
|
|
932
|
+
from superqode.commands.acp import connect_agent
|
|
933
|
+
|
|
934
|
+
exit(connect_agent(agent, project_dir))
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
@agents.command("install")
|
|
938
|
+
@click.argument("agent", metavar="AGENT")
|
|
939
|
+
def agents_install(agent):
|
|
940
|
+
"""Install an ACP coding agent."""
|
|
941
|
+
from superqode.commands.acp import install_agent_cmd
|
|
942
|
+
|
|
943
|
+
exit(install_agent_cmd(agent))
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
@cli_main.group()
|
|
947
|
+
def connect():
|
|
948
|
+
"""Connect to models via ACP agents, BYOK providers, or LOCAL providers."""
|
|
949
|
+
pass
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
@connect.command("acp")
|
|
953
|
+
@click.argument("agent", metavar="AGENT")
|
|
954
|
+
@click.option("--project-dir", "-d", metavar="DIR", help="Project directory to work in")
|
|
955
|
+
def connect_acp(agent, project_dir):
|
|
956
|
+
"""Connect to an ACP coding agent."""
|
|
957
|
+
from superqode.commands.acp import connect_agent
|
|
958
|
+
|
|
959
|
+
exit(connect_agent(agent, project_dir))
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
@connect.command("byok")
|
|
963
|
+
@click.argument("provider", metavar="PROVIDER", required=False)
|
|
964
|
+
@click.argument("model", metavar="MODEL", required=False)
|
|
965
|
+
def connect_byok(provider, model):
|
|
966
|
+
"""Connect to a BYOK provider/model."""
|
|
967
|
+
from superqode.commands.providers import connect_provider
|
|
968
|
+
|
|
969
|
+
exit(connect_provider(provider, model))
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
@connect.command("local")
|
|
973
|
+
@click.argument("provider", metavar="PROVIDER", required=False)
|
|
974
|
+
@click.argument("model", metavar="MODEL", required=False)
|
|
975
|
+
def connect_local(provider, model):
|
|
976
|
+
"""Connect to a local/self-hosted provider/model."""
|
|
977
|
+
from superqode.commands.providers import connect_local_provider
|
|
978
|
+
|
|
979
|
+
exit(connect_local_provider(provider, model))
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
# Alias for backward compatibility
|
|
983
|
+
main = cli_main
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
# Simple toast replacement since UI components were removed
|
|
987
|
+
class ToastType:
|
|
988
|
+
SUCCESS = "success"
|
|
989
|
+
ERROR = "error"
|
|
990
|
+
INFO = "info"
|
|
991
|
+
WARNING = "warning"
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def show_toast(message: str, toast_type: str) -> None:
|
|
995
|
+
"""Simple toast replacement - just print the message."""
|
|
996
|
+
if toast_type == ToastType.SUCCESS:
|
|
997
|
+
_console.print(f"[green]✓ {message}[/green]")
|
|
998
|
+
elif toast_type == ToastType.ERROR:
|
|
999
|
+
_console.print(f"[red]✗ {message}[/red]")
|
|
1000
|
+
elif toast_type == ToastType.WARNING:
|
|
1001
|
+
_console.print(f"[yellow]⚠️ {message}[/yellow]")
|
|
1002
|
+
else:
|
|
1003
|
+
_console.print(f"[blue]ℹ️ {message}[/blue]")
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
from rich.text import Text
|
|
1007
|
+
from rich.panel import Panel
|
|
1008
|
+
from rich.live import Live
|
|
1009
|
+
from rich.spinner import Spinner
|
|
1010
|
+
from rich.markdown import Markdown
|
|
1011
|
+
from rich.syntax import Syntax
|
|
1012
|
+
from rich.align import Align
|
|
1013
|
+
from rich.box import DOUBLE, ROUNDED
|
|
1014
|
+
from rich.columns import Columns
|
|
1015
|
+
from rich.table import Table
|
|
1016
|
+
from rich.markup import escape
|
|
1017
|
+
import rich.box
|
|
1018
|
+
|
|
1019
|
+
from superqode import __version__
|
|
1020
|
+
from superqode.providers import ProviderManager
|
|
1021
|
+
from superqode.dialogs import ProviderDialog, ModelDialog, ConnectDialog
|
|
1022
|
+
from superqode.tui import (
|
|
1023
|
+
SuperQodeUI,
|
|
1024
|
+
ThinkingSpinner,
|
|
1025
|
+
ResponsePanel,
|
|
1026
|
+
print_disconnect_message,
|
|
1027
|
+
print_exit_message,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
# Alias for backward compatibility
|
|
1031
|
+
SuperQodeTUI = SuperQodeUI
|
|
1032
|
+
|
|
1033
|
+
# LLM provider management
|
|
1034
|
+
from superqode.providers.manager import ProviderManager
|
|
1035
|
+
|
|
1036
|
+
# Register new BYOK provider and agent commands
|
|
1037
|
+
from superqode.commands.providers import providers as providers_cmd
|
|
1038
|
+
from superqode.commands.agents import agents as agents_cmd_new
|
|
1039
|
+
from superqode.commands.auth import auth as auth_cmd
|
|
1040
|
+
from superqode.commands.qe import qe as qe_cmd
|
|
1041
|
+
from superqode.commands.roles import roles as roles_cmd
|
|
1042
|
+
from superqode.commands.suggestions import suggestions as suggestions_cmd
|
|
1043
|
+
from superqode.commands.serve import serve as serve_cmd
|
|
1044
|
+
|
|
1045
|
+
# Add provider commands (superqode providers list, superqode providers show, etc.)
|
|
1046
|
+
cli_main.add_command(providers_cmd, name="providers")
|
|
1047
|
+
|
|
1048
|
+
# Add auth commands (superqode auth info, superqode auth check, etc.)
|
|
1049
|
+
cli_main.add_command(auth_cmd, name="auth")
|
|
1050
|
+
|
|
1051
|
+
# Add QE commands (superqode qe ...)
|
|
1052
|
+
cli_main.add_command(qe_cmd, name="qe")
|
|
1053
|
+
|
|
1054
|
+
# Add roles commands (superqode roles list, superqode roles info, etc.)
|
|
1055
|
+
cli_main.add_command(roles_cmd, name="roles")
|
|
1056
|
+
|
|
1057
|
+
# Add suggestions commands (superqode suggestions list, superqode suggestions apply, etc.)
|
|
1058
|
+
cli_main.add_command(suggestions_cmd, name="suggestions")
|
|
1059
|
+
|
|
1060
|
+
# Add Server commands (superqode serve lsp, superqode serve web, etc.)
|
|
1061
|
+
cli_main.add_command(serve_cmd, name="serve")
|
|
1062
|
+
|
|
1063
|
+
# Note: agents command already exists, so we add the new one with a different approach
|
|
1064
|
+
# The existing agents command handles ACP agents, we'll enhance it
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
if __name__ == "__main__":
|
|
1068
|
+
"""Entry point for the SuperQode CLI."""
|
|
1069
|
+
cli_main()
|