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.
- easyclaw_tui-0.1.0/PKG-INFO +20 -0
- easyclaw_tui-0.1.0/pyproject.toml +34 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/__init__.py +3 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/api/__init__.py +0 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/api/client.py +123 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/app.py +61 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/cli.py +93 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/screens/__init__.py +0 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/screens/help.py +125 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/screens/install.py +157 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/state.py +98 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/__init__.py +0 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/base.py +76 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s01_preflight.py +71 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s02_nodejs.py +54 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s03_openclaw.py +35 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s04_tools.py +29 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s05_config.py +80 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s06_token.py +18 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s07_write_config.py +90 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s08_env_vars.py +38 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s09_workspace.py +102 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s10_validate.py +32 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s11_init_gateway.py +21 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s12_systemd.py +67 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s13_mcp.py +39 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s14_channel.py +48 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/steps/s15_health.py +111 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/styles/app.tcss +264 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/__init__.py +0 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/chat_panel.py +55 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/config_form.py +187 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/option_selector.py +51 -0
- easyclaw_tui-0.1.0/src/easyclaw_tui/widgets/step_panel.py +86 -0
- 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"]
|
|
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()
|
|
File without changes
|
|
@@ -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}")
|