easyclaw-tui 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. easyclaw_tui-0.1.0/PKG-INFO +20 -0
  2. easyclaw_tui-0.1.0/pyproject.toml +34 -0
  3. easyclaw_tui-0.1.0/src/easyclaw_tui/__init__.py +3 -0
  4. easyclaw_tui-0.1.0/src/easyclaw_tui/api/__init__.py +0 -0
  5. easyclaw_tui-0.1.0/src/easyclaw_tui/api/client.py +123 -0
  6. easyclaw_tui-0.1.0/src/easyclaw_tui/app.py +61 -0
  7. easyclaw_tui-0.1.0/src/easyclaw_tui/cli.py +93 -0
  8. easyclaw_tui-0.1.0/src/easyclaw_tui/screens/__init__.py +0 -0
  9. easyclaw_tui-0.1.0/src/easyclaw_tui/screens/help.py +125 -0
  10. easyclaw_tui-0.1.0/src/easyclaw_tui/screens/install.py +157 -0
  11. easyclaw_tui-0.1.0/src/easyclaw_tui/state.py +98 -0
  12. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/__init__.py +0 -0
  13. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/base.py +76 -0
  14. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s01_preflight.py +71 -0
  15. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s02_nodejs.py +54 -0
  16. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s03_openclaw.py +35 -0
  17. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s04_tools.py +29 -0
  18. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s05_config.py +80 -0
  19. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s06_token.py +18 -0
  20. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s07_write_config.py +90 -0
  21. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s08_env_vars.py +38 -0
  22. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s09_workspace.py +102 -0
  23. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s10_validate.py +32 -0
  24. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s11_init_gateway.py +21 -0
  25. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s12_systemd.py +67 -0
  26. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s13_mcp.py +39 -0
  27. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s14_channel.py +48 -0
  28. easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s15_health.py +111 -0
  29. easyclaw_tui-0.1.0/src/easyclaw_tui/styles/app.tcss +264 -0
  30. easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/__init__.py +0 -0
  31. easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/chat_panel.py +55 -0
  32. easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/config_form.py +187 -0
  33. easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/option_selector.py +51 -0
  34. easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/step_panel.py +86 -0
  35. easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/step_progress.py +49 -0
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyclaw-tui
3
+ Version: 0.1.0
4
+ Summary: Interactive terminal installer for OpenClaw — powered by EasyClaw
5
+ Project-URL: Homepage, https://easyclaw.help
6
+ Project-URL: Repository, https://github.com/easyclaw/easyclaw-tui
7
+ Author-email: EasyClaw <johnny@easyclaw.help>
8
+ License-Expression: MIT
9
+ Keywords: easyclaw,installer,openclaw,tui
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: System :: Installation/Setup
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: httpx<1.0.0,>=0.27.0
17
+ Requires-Dist: textual<4.0.0,>=3.0.0
18
+ Description-Content-Type: text/plain
19
+
20
+ EasyClaw TUI — Interactive OpenClaw installer.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "easyclaw-tui"
7
+ version = "0.1.0"
8
+ description = "Interactive terminal installer for OpenClaw — powered by EasyClaw"
9
+ readme = {text = "EasyClaw TUI — Interactive OpenClaw installer.", content-type = "text/plain"}
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "EasyClaw", email = "johnny@easyclaw.help" }]
13
+ keywords = ["openclaw", "easyclaw", "installer", "tui"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: System :: Installation/Setup",
20
+ ]
21
+ dependencies = [
22
+ "textual>=3.0.0,<4.0.0",
23
+ "httpx>=0.27.0,<1.0.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ easyclaw = "easyclaw_tui.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://easyclaw.help"
31
+ Repository = "https://github.com/easyclaw/easyclaw-tui"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/easyclaw_tui"]
@@ -0,0 +1,3 @@
1
+ """EasyClaw TUI — Interactive OpenClaw installer."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,123 @@
1
+ """MCP JSON-RPC client for EasyClaw Foreman chat."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ MCP_URL = "https://mcp.easyclaw.help/mcp"
11
+
12
+ # Keywords that signal which MCP tool to route to
13
+ _GATEWAY_KEYWORDS = re.compile(
14
+ r"gateway|bind|port|421|proxy|traefik|reverse proxy|https|tls|ssl|certificate|misdirected",
15
+ re.IGNORECASE,
16
+ )
17
+ _DIAGNOSE_KEYWORDS = re.compile(
18
+ r"error|fail|crash|broken|not working|won't start|can't connect|timeout|refused|"
19
+ r"exit code|traceback|exception|502|503|404|ECONNREFUSED",
20
+ re.IGNORECASE,
21
+ )
22
+ _SECURITY_KEYWORDS = re.compile(
23
+ r"security|audit|hardening|firewall|allowlist|dmpolicy|token|permission|auth",
24
+ re.IGNORECASE,
25
+ )
26
+
27
+
28
+ def route_query(text: str) -> tuple[str, dict[str, Any]]:
29
+ """Pick the best MCP tool for a user's chat message."""
30
+ if _GATEWAY_KEYWORDS.search(text):
31
+ return "troubleshoot_gateway", {"symptom": text}
32
+ if _DIAGNOSE_KEYWORDS.search(text):
33
+ return "diagnose_issue", {"error_text": text}
34
+ if _SECURITY_KEYWORDS.search(text):
35
+ return "security_audit", {}
36
+ # Default to knowledge search
37
+ return "search_knowledge", {"query": text}
38
+
39
+
40
+ class MCPClient:
41
+ """Sends JSON-RPC 2.0 requests to the EasyClaw MCP server."""
42
+
43
+ def __init__(self, api_key: str, base_url: str = MCP_URL) -> None:
44
+ self.api_key = api_key
45
+ self.base_url = base_url
46
+ self._id = 0
47
+
48
+ def _next_id(self) -> int:
49
+ self._id += 1
50
+ return self._id
51
+
52
+ async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
53
+ """Call an MCP tool and return the result."""
54
+ payload = {
55
+ "jsonrpc": "2.0",
56
+ "id": self._next_id(),
57
+ "method": "tools/call",
58
+ "params": {
59
+ "name": tool_name,
60
+ "arguments": arguments or {},
61
+ },
62
+ }
63
+ headers = {
64
+ "Content-Type": "application/json",
65
+ "Authorization": f"Bearer {self.api_key}",
66
+ }
67
+ async with httpx.AsyncClient(timeout=30.0) as client:
68
+ resp = await client.post(self.base_url, json=payload, headers=headers)
69
+ resp.raise_for_status()
70
+ data = resp.json()
71
+
72
+ if "error" in data:
73
+ return {"error": data["error"].get("message", str(data["error"]))}
74
+
75
+ result = data.get("result", {})
76
+ # MCP returns content array — extract text
77
+ if isinstance(result, dict) and "content" in result:
78
+ texts = [c.get("text", "") for c in result["content"] if c.get("type") == "text"]
79
+ return {"text": "\n".join(texts)}
80
+ return result
81
+
82
+ async def smart_query(self, text: str) -> str:
83
+ """Route a user message to the best MCP tool and return the response."""
84
+ tool_name, args = route_query(text)
85
+ result = await self.call_tool(tool_name, args)
86
+ return result.get("text", result.get("error", "No response"))
87
+
88
+ async def diagnose(self, error_text: str) -> str:
89
+ """Diagnose an issue using the MCP server."""
90
+ result = await self.call_tool("diagnose_issue", {"error_text": error_text})
91
+ return result.get("text", result.get("error", "No diagnosis available"))
92
+
93
+ async def search_knowledge(self, query: str) -> str:
94
+ """Search the knowledge base."""
95
+ result = await self.call_tool("search_knowledge", {"query": query})
96
+ return result.get("text", result.get("error", "No results"))
97
+
98
+ async def troubleshoot_gateway(self, symptom: str) -> str:
99
+ """Troubleshoot gateway issues."""
100
+ result = await self.call_tool("troubleshoot_gateway", {"symptom": symptom})
101
+ return result.get("text", result.get("error", "No troubleshooting info"))
102
+
103
+ async def validate_deployment(self) -> str:
104
+ """Run a full deployment health check."""
105
+ result = await self.call_tool("validate_deployment", {})
106
+ return result.get("text", result.get("error", "No validation data"))
107
+
108
+ async def security_audit(self) -> str:
109
+ """Run a security audit."""
110
+ result = await self.call_tool("security_audit", {})
111
+ return result.get("text", result.get("error", "No audit data"))
112
+
113
+ async def health_check(self) -> bool:
114
+ """Check if the MCP server is reachable."""
115
+ try:
116
+ async with httpx.AsyncClient(timeout=10.0) as client:
117
+ resp = await client.get(
118
+ self.base_url.replace("/mcp", "/health"),
119
+ headers={"Authorization": f"Bearer {self.api_key}"},
120
+ )
121
+ return resp.status_code == 200
122
+ except Exception:
123
+ return False
@@ -0,0 +1,61 @@
1
+ """Main EasyClaw TUI application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import App, ComposeResult
6
+ from textual.binding import Binding
7
+
8
+ from easyclaw_tui.state import InstallState
9
+
10
+ # Re-export for convenience
11
+ from easyclaw_tui.state import StepStatus, StepInfo, MODEL_MAP # noqa: F401
12
+
13
+
14
+ class EasyClawApp(App):
15
+ """EasyClaw interactive installer."""
16
+
17
+ TITLE = "EasyClaw Foreman"
18
+ SUB_TITLE = "OpenClaw Installer"
19
+ CSS_PATH = "styles/app.tcss"
20
+
21
+ BINDINGS = [
22
+ Binding("q", "quit", "Quit", show=True),
23
+ Binding("ctrl+c", "quit", "Quit", show=False),
24
+ Binding("f1", "toggle_chat", "Foreman Chat", show=True),
25
+ ]
26
+
27
+ def __init__(
28
+ self,
29
+ mode: str = "install",
30
+ api_key: str = "",
31
+ openrouter_key: str = "",
32
+ openai_key: str = "",
33
+ model: str = "minimax-m2.5",
34
+ channel: str = "none",
35
+ bind: str = "lan",
36
+ ) -> None:
37
+ super().__init__()
38
+ self.install_state = InstallState(
39
+ api_key=api_key,
40
+ openrouter_key=openrouter_key,
41
+ openai_key=openai_key,
42
+ model=model,
43
+ channel=channel,
44
+ bind=bind,
45
+ )
46
+ self.mode = mode
47
+
48
+ def on_mount(self) -> None:
49
+ if self.mode == "help":
50
+ from easyclaw_tui.screens.help import HelpScreen
51
+
52
+ self.push_screen(HelpScreen(self.install_state))
53
+ else:
54
+ from easyclaw_tui.screens.install import InstallScreen
55
+
56
+ self.push_screen(InstallScreen(self.install_state))
57
+
58
+ def action_toggle_chat(self) -> None:
59
+ screen = self.screen
60
+ if hasattr(screen, "toggle_chat"):
61
+ screen.toggle_chat()
@@ -0,0 +1,93 @@
1
+ """CLI entry point: easyclaw install / easyclaw help."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(
11
+ prog="easyclaw",
12
+ description="EasyClaw — Interactive OpenClaw Installer",
13
+ )
14
+ sub = parser.add_subparsers(dest="command")
15
+
16
+ # easyclaw install
17
+ install_p = sub.add_parser("install", help="Run the interactive installer")
18
+ install_p.add_argument("--api-key", default="", help="EasyClaw API key (ec_live_...)")
19
+ install_p.add_argument("--openrouter-key", default="", help="OpenRouter API key (sk-or-...)")
20
+ install_p.add_argument("--openai-key", default="", help="OpenAI API key (optional, for memory)")
21
+ install_p.add_argument("--model", default="minimax-m2.5", help="Primary model (default: minimax-m2.5)")
22
+ install_p.add_argument("--channel", default="none", help="Channel: none, whatsapp, telegram, discord")
23
+ install_p.add_argument("--bind", default="lan", help="Bind mode: lan, loopback, auto, tailnet")
24
+ install_p.add_argument("--no-tui", action="store_true", help="Fall back to bash script")
25
+
26
+ # easyclaw help
27
+ sub.add_parser("help", help="Post-install troubleshooting and diagnostics")
28
+
29
+ args = parser.parse_args()
30
+
31
+ if args.command is None:
32
+ # Default to install
33
+ args.command = "install"
34
+ args.api_key = ""
35
+ args.openrouter_key = ""
36
+ args.openai_key = ""
37
+ args.model = "minimax-m2.5"
38
+ args.channel = "none"
39
+ args.bind = "lan"
40
+ args.no_tui = False
41
+
42
+ if args.command == "install":
43
+ if getattr(args, "no_tui", False):
44
+ print("Falling back to bash installer...")
45
+ print("Run: curl -fsSL https://easyclaw.help/install.sh | bash")
46
+ sys.exit(0)
47
+ _run_install(
48
+ api_key=getattr(args, "api_key", ""),
49
+ openrouter_key=getattr(args, "openrouter_key", ""),
50
+ openai_key=getattr(args, "openai_key", ""),
51
+ model=getattr(args, "model", "minimax-m2.5"),
52
+ channel=getattr(args, "channel", "none"),
53
+ bind=getattr(args, "bind", "lan"),
54
+ )
55
+
56
+ elif args.command == "help":
57
+ _run_help()
58
+
59
+ else:
60
+ parser.print_help()
61
+
62
+
63
+ def _run_install(
64
+ api_key: str = "",
65
+ openrouter_key: str = "",
66
+ openai_key: str = "",
67
+ model: str = "minimax-m2.5",
68
+ channel: str = "none",
69
+ bind: str = "lan",
70
+ ) -> None:
71
+ from easyclaw_tui.app import EasyClawApp
72
+
73
+ app = EasyClawApp(
74
+ mode="install",
75
+ api_key=api_key,
76
+ openrouter_key=openrouter_key,
77
+ openai_key=openai_key,
78
+ model=model,
79
+ channel=channel,
80
+ bind=bind,
81
+ )
82
+ app.run()
83
+
84
+
85
+ def _run_help() -> None:
86
+ from easyclaw_tui.app import EasyClawApp
87
+
88
+ app = EasyClawApp(mode="help")
89
+ app.run()
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -0,0 +1,125 @@
1
+ """Help screen — post-install troubleshooting and diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.screen import Screen
7
+ from textual.widgets import Header, Footer, Static, RichLog, Input, Button
8
+ from textual.containers import Vertical, Horizontal
9
+
10
+ from easyclaw_tui.state import InstallState
11
+ from easyclaw_tui.api.client import MCPClient
12
+
13
+
14
+ QUICK_ACTIONS = [
15
+ ("validate", "Validate Deployment", "validate_deployment"),
16
+ ("security", "Security Audit", "security_audit"),
17
+ ("gateway", "Troubleshoot Gateway", "troubleshoot_gateway"),
18
+ ]
19
+
20
+
21
+ class HelpScreen(Screen):
22
+ """Post-install troubleshooting with Foreman chat and quick actions."""
23
+
24
+ BINDINGS = [
25
+ ("escape", "app.pop_screen", "Back"),
26
+ ]
27
+
28
+ def __init__(self, state: InstallState) -> None:
29
+ super().__init__()
30
+ self.state = state
31
+
32
+ def compose(self) -> ComposeResult:
33
+ yield Header()
34
+ with Vertical(id="help-container"):
35
+ yield Static(
36
+ "[bold #f59e0b]EasyClaw Foreman — Help & Diagnostics[/bold #f59e0b]",
37
+ id="help-title",
38
+ )
39
+ yield Static(
40
+ "Ask a question or run a quick diagnostic below.",
41
+ id="help-subtitle",
42
+ )
43
+
44
+ # Quick action buttons
45
+ with Horizontal(id="quick-actions"):
46
+ for action_id, label, _ in QUICK_ACTIONS:
47
+ yield Button(label, id=f"btn-{action_id}", variant="primary")
48
+
49
+ yield RichLog(id="help-log", wrap=True, highlight=True, markup=True)
50
+
51
+ # Chat input
52
+ with Horizontal(id="help-input-container"):
53
+ yield Input(
54
+ placeholder="Describe your issue or ask a question...",
55
+ id="help-input",
56
+ )
57
+ yield Button("Ask", id="btn-ask", variant="primary")
58
+
59
+ yield Footer()
60
+
61
+ @property
62
+ def log(self) -> RichLog:
63
+ return self.query_one("#help-log", RichLog)
64
+
65
+ def _get_client(self) -> MCPClient | None:
66
+ if not self.state.api_key:
67
+ self.log.write("[red]No API key set. Run the installer first.[/red]")
68
+ return None
69
+ return MCPClient(self.state.api_key)
70
+
71
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
72
+ btn_id = event.button.id
73
+
74
+ if btn_id == "btn-ask":
75
+ await self._handle_ask()
76
+ elif btn_id == "btn-validate":
77
+ await self._run_action("Validating deployment...", "validate_deployment")
78
+ elif btn_id == "btn-security":
79
+ await self._run_action("Running security audit...", "security_audit")
80
+ elif btn_id == "btn-gateway":
81
+ inp = self.query_one("#help-input", Input)
82
+ symptom = inp.value.strip() or "gateway not responding"
83
+ await self._run_action(
84
+ f"Troubleshooting gateway: {symptom}",
85
+ "troubleshoot_gateway",
86
+ {"symptom": symptom},
87
+ )
88
+
89
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
90
+ if event.input.id == "help-input":
91
+ await self._handle_ask()
92
+
93
+ async def _handle_ask(self) -> None:
94
+ inp = self.query_one("#help-input", Input)
95
+ text = inp.value.strip()
96
+ if not text:
97
+ return
98
+ inp.value = ""
99
+
100
+ self.log.write(f"\n[bold cyan]You:[/bold cyan] {text}")
101
+ client = self._get_client()
102
+ if not client:
103
+ return
104
+
105
+ self.log.write("[dim]Thinking...[/dim]")
106
+ try:
107
+ response = await client.smart_query(text)
108
+ self.log.write(f"[bold #f59e0b]Foreman:[/bold #f59e0b] {response}")
109
+ except Exception as e:
110
+ self.log.write(f"[red]Error: {e}[/red]")
111
+
112
+ async def _run_action(
113
+ self, label: str, tool: str, args: dict | None = None
114
+ ) -> None:
115
+ self.log.write(f"\n[cyan]→ {label}[/cyan]")
116
+ client = self._get_client()
117
+ if not client:
118
+ return
119
+
120
+ try:
121
+ result = await client.call_tool(tool, args or {})
122
+ text = result.get("text", result.get("error", "No response"))
123
+ self.log.write(f"[bold #f59e0b]Foreman:[/bold #f59e0b]\n{text}")
124
+ except Exception as e:
125
+ self.log.write(f"[red]Error: {e}[/red]")
@@ -0,0 +1,157 @@
1
+ """Install screen — orchestrates the 15-step install flow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ from textual.app import ComposeResult
8
+ from textual.screen import Screen
9
+ from textual.widgets import Header, Footer, Static
10
+ from textual.containers import Container
11
+
12
+ from easyclaw_tui.state import InstallState, StepStatus
13
+ from easyclaw_tui.widgets.step_progress import StepProgress
14
+ from easyclaw_tui.widgets.step_panel import StepPanel
15
+ from easyclaw_tui.widgets.chat_panel import ChatPanel
16
+ from easyclaw_tui.api.client import MCPClient
17
+
18
+ from easyclaw_tui.steps.base import StepResult
19
+ from easyclaw_tui.steps.s01_preflight import PreflightStep
20
+ from easyclaw_tui.steps.s02_nodejs import NodejsStep
21
+ from easyclaw_tui.steps.s03_openclaw import OpenClawStep
22
+ from easyclaw_tui.steps.s04_tools import ToolsStep
23
+ from easyclaw_tui.steps.s05_config import ConfigStep
24
+ from easyclaw_tui.steps.s06_token import TokenStep
25
+ from easyclaw_tui.steps.s07_write_config import WriteConfigStep
26
+ from easyclaw_tui.steps.s08_env_vars import EnvVarsStep
27
+ from easyclaw_tui.steps.s09_workspace import WorkspaceStep
28
+ from easyclaw_tui.steps.s10_validate import ValidateStep
29
+ from easyclaw_tui.steps.s11_init_gateway import InitGatewayStep
30
+ from easyclaw_tui.steps.s12_systemd import SystemdStep
31
+ from easyclaw_tui.steps.s13_mcp import McpStep
32
+ from easyclaw_tui.steps.s14_channel import ChannelStep
33
+ from easyclaw_tui.steps.s15_health import HealthStep
34
+
35
+
36
+ class InstallScreen(Screen):
37
+ """Main install screen with sidebar, content panel, and chat."""
38
+
39
+ BINDINGS = []
40
+
41
+ def __init__(self, state: InstallState) -> None:
42
+ super().__init__()
43
+ self.state = state
44
+ self._step_classes = [
45
+ PreflightStep,
46
+ NodejsStep,
47
+ OpenClawStep,
48
+ ToolsStep,
49
+ ConfigStep,
50
+ TokenStep,
51
+ WriteConfigStep,
52
+ EnvVarsStep,
53
+ WorkspaceStep,
54
+ ValidateStep,
55
+ InitGatewayStep,
56
+ SystemdStep,
57
+ McpStep,
58
+ ChannelStep,
59
+ HealthStep,
60
+ ]
61
+ self._chat_visible = False
62
+
63
+ def compose(self) -> ComposeResult:
64
+ yield Header()
65
+ yield StepProgress(self.state)
66
+ yield StepPanel(self.state)
67
+ yield ChatPanel()
68
+ yield Footer()
69
+
70
+ @property
71
+ def progress(self) -> StepProgress:
72
+ return self.query_one(StepProgress)
73
+
74
+ @property
75
+ def panel(self) -> StepPanel:
76
+ return self.query_one(StepPanel)
77
+
78
+ @property
79
+ def chat(self) -> ChatPanel:
80
+ return self.query_one(ChatPanel)
81
+
82
+ def toggle_chat(self) -> None:
83
+ self._chat_visible = not self._chat_visible
84
+ chat = self.chat
85
+ if self._chat_visible:
86
+ chat.add_class("--visible")
87
+ else:
88
+ chat.remove_class("--visible")
89
+
90
+ async def on_mount(self) -> None:
91
+ # Start the install flow
92
+ self.run_worker(self._run_install())
93
+
94
+ async def _run_install(self) -> None:
95
+ """Execute steps sequentially."""
96
+ for i, step_cls in enumerate(self._step_classes):
97
+ step_num = i + 1
98
+ step_info = self.state.steps[i]
99
+
100
+ # Update UI
101
+ self.state.set_step_status(step_num, StepStatus.RUNNING)
102
+ self.progress.refresh_steps()
103
+ self.panel.set_step(step_num, step_info.name)
104
+
105
+ # Create and run the step
106
+ step = step_cls(self.state, self.panel)
107
+ try:
108
+ result = await step.run()
109
+ except Exception as e:
110
+ self.panel.log_error(f"Step failed with exception: {e}")
111
+ result = StepResult(success=False, message=str(e))
112
+
113
+ # Update status
114
+ if result.success:
115
+ self.state.set_step_status(step_num, StepStatus.SUCCESS)
116
+ elif result.skipped:
117
+ self.state.set_step_status(step_num, StepStatus.SKIPPED)
118
+ else:
119
+ self.state.set_step_status(step_num, StepStatus.FAILED)
120
+ self.panel.log_error(f"Step {step_num} failed: {result.message}")
121
+ # On failure, offer Foreman diagnosis
122
+ if self.state.api_key:
123
+ self.panel.log_info("Asking Foreman for help...")
124
+ try:
125
+ client = MCPClient(self.state.api_key)
126
+ diagnosis = await client.diagnose(result.message)
127
+ self.panel.log_raw(f"\n[bold #f59e0b]Foreman says:[/bold #f59e0b]\n{diagnosis}")
128
+ except Exception:
129
+ self.panel.log_warn("Could not reach Foreman — check API key")
130
+ # Ask whether to continue or abort
131
+ self.panel.log_raw("\n[bold]Press Enter to continue or Ctrl+C to abort[/bold]")
132
+ # Continue to next step after brief pause
133
+ await asyncio.sleep(2)
134
+
135
+ self.progress.refresh_steps()
136
+ self.state.current_step = step_num + 1
137
+
138
+ # All done
139
+ self.panel.log_raw("\n[bold green]Installation complete![/bold green]")
140
+
141
+ async def on_chat_panel_chat_submitted(self, event: ChatPanel.ChatSubmitted) -> None:
142
+ """Handle chat messages by routing to the best MCP tool."""
143
+ if not self.state.api_key:
144
+ self.chat.add_system_message("Set your API key first (Step 5)")
145
+ return
146
+
147
+ from easyclaw_tui.api.client import route_query
148
+
149
+ tool_name, _ = route_query(event.text)
150
+ self.chat.add_system_message(f"Using {tool_name}...")
151
+
152
+ try:
153
+ client = MCPClient(self.state.api_key)
154
+ response = await client.smart_query(event.text)
155
+ self.chat.add_bot_message(response)
156
+ except Exception as e:
157
+ self.chat.add_system_message(f"Error: {e}")