openagent-framework 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/PKG-INFO +1 -1
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/__init__.py +1 -1
- openagent_framework-0.2.3/openagent/bootstrap.py +364 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/cli.py +130 -19
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/scheduler.py +18 -13
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/PKG-INFO +1 -1
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/SOURCES.txt +1 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/pyproject.toml +1 -1
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/docs/README.md +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/agent.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/__init__.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/base.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/discord.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/senders.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/telegram.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/channels/whatsapp.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/config.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcp/__init__.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcp/client.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcp/oauth.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/chrome-devtools/.gitignore +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/chrome-devtools/package.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/.gitignore +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/package.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/index.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/main.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/tools/computer.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/tools/index.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/utils/response.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/xdotoolStringToKeys.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/tsconfig.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/.gitignore +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/package.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/src/index.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/editor/tsconfig.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/.gitignore +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/index.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/package.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/tsconfig.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/.gitignore +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/package.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/src/index.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/shell/tsconfig.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/.gitignore +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/package.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/browser-pool.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/content-extractor.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/enhanced-content-extractor.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/index.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/rate-limiter.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/search-engine.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/types.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/utils.ts +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/tsconfig.json +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/memory/__init__.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/memory/db.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/memory/manager.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/__init__.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/base.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/claude_api.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/claude_cli.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/models/zhipu.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/server.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/service.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/__init__.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/base.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/manager.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/services/obsidian.py +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/dependency_links.txt +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/entry_points.txt +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/requires.txt +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/top_level.txt +0 -0
- {openagent_framework-0.2.1 → openagent_framework-0.2.3}/setup.cfg +0 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Platform detection, environment checks and dependency installers.
|
|
2
|
+
|
|
3
|
+
Powers `openagent doctor` and the extended `openagent setup` command.
|
|
4
|
+
Aim: make it as easy as possible to get OpenAgent running on a fresh
|
|
5
|
+
machine, across Linux / macOS / Windows, *without* pretending we can
|
|
6
|
+
silently install Docker Desktop on Mac/Win (we can't, legally or
|
|
7
|
+
practically).
|
|
8
|
+
|
|
9
|
+
Design:
|
|
10
|
+
- Every check returns a `Check` dataclass — name, status, message, fix hint.
|
|
11
|
+
- Installers either succeed, raise, or return "manual-instructions" strings.
|
|
12
|
+
- The caller (CLI) decides how to present the result.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import platform
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Check result dataclass ──
|
|
31
|
+
|
|
32
|
+
Status = str # "ok" | "warn" | "fail" | "skip"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Check:
|
|
37
|
+
name: str
|
|
38
|
+
status: Status
|
|
39
|
+
message: str
|
|
40
|
+
fix_hint: str = ""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def ok(self) -> bool:
|
|
44
|
+
return self.status == "ok"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Report:
|
|
49
|
+
checks: list[Check] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def add(self, chk: Check) -> None:
|
|
52
|
+
self.checks.append(chk)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def has_failures(self) -> bool:
|
|
56
|
+
return any(c.status == "fail" for c in self.checks)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def has_warnings(self) -> bool:
|
|
60
|
+
return any(c.status == "warn" for c in self.checks)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── Platform helpers ──
|
|
64
|
+
|
|
65
|
+
def current_platform() -> str:
|
|
66
|
+
system = platform.system()
|
|
67
|
+
if system == "Darwin":
|
|
68
|
+
return "macos"
|
|
69
|
+
if system == "Linux":
|
|
70
|
+
return "linux"
|
|
71
|
+
if system == "Windows":
|
|
72
|
+
return "windows"
|
|
73
|
+
return system.lower()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def detect_linux_pkg_manager() -> str | None:
|
|
77
|
+
"""Return 'apt' / 'dnf' / 'pacman' / None."""
|
|
78
|
+
for mgr in ("apt-get", "dnf", "pacman", "zypper", "apk"):
|
|
79
|
+
if shutil.which(mgr):
|
|
80
|
+
return mgr.replace("-get", "")
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ── Individual checks ──
|
|
85
|
+
|
|
86
|
+
def check_python() -> Check:
|
|
87
|
+
v = sys.version_info
|
|
88
|
+
ver = f"{v.major}.{v.minor}.{v.micro}"
|
|
89
|
+
if v >= (3, 11):
|
|
90
|
+
return Check("python", "ok", f"Python {ver}")
|
|
91
|
+
return Check(
|
|
92
|
+
"python", "fail",
|
|
93
|
+
f"Python {ver} — OpenAgent requires 3.11+",
|
|
94
|
+
"Install Python 3.11 or newer.",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_command(cmd: str, name: str | None = None) -> Check:
|
|
99
|
+
display = name or cmd
|
|
100
|
+
path = shutil.which(cmd)
|
|
101
|
+
if not path:
|
|
102
|
+
return Check(display, "warn", f"{display} not installed", f"Install {display}.")
|
|
103
|
+
try:
|
|
104
|
+
out = subprocess.run(
|
|
105
|
+
[cmd, "--version"], capture_output=True, text=True, timeout=5,
|
|
106
|
+
)
|
|
107
|
+
version_line = (out.stdout or out.stderr).strip().splitlines()[0] if (out.stdout or out.stderr) else "installed"
|
|
108
|
+
return Check(display, "ok", version_line)
|
|
109
|
+
except Exception:
|
|
110
|
+
return Check(display, "ok", f"{display} at {path}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def check_docker() -> Check:
|
|
114
|
+
"""Check Docker CLI + daemon reachability."""
|
|
115
|
+
if not shutil.which("docker"):
|
|
116
|
+
hint = {
|
|
117
|
+
"linux": "Run: openagent setup --with-docker",
|
|
118
|
+
"macos": "Install Docker Desktop: https://docs.docker.com/desktop/install/mac-install/",
|
|
119
|
+
"windows": "Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/",
|
|
120
|
+
}.get(current_platform(), "Install Docker.")
|
|
121
|
+
return Check("docker", "warn", "docker CLI not found", hint)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
proc = subprocess.run(
|
|
125
|
+
["docker", "info", "--format", "{{.ServerVersion}}"],
|
|
126
|
+
capture_output=True, text=True, timeout=5,
|
|
127
|
+
)
|
|
128
|
+
if proc.returncode != 0:
|
|
129
|
+
hint = "Start the Docker daemon."
|
|
130
|
+
if current_platform() in ("macos", "windows"):
|
|
131
|
+
hint = "Launch Docker Desktop and wait for it to be ready."
|
|
132
|
+
return Check(
|
|
133
|
+
"docker", "warn",
|
|
134
|
+
"docker CLI found, but daemon is not reachable",
|
|
135
|
+
hint,
|
|
136
|
+
)
|
|
137
|
+
return Check("docker", "ok", f"Docker daemon {proc.stdout.strip()} reachable")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
return Check("docker", "warn", f"docker check failed: {e}", "")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_git() -> Check:
|
|
143
|
+
return check_command("git")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def check_node() -> Check:
|
|
147
|
+
return check_command("node")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def check_openagent_config(config_path: Path) -> Check:
|
|
151
|
+
if not config_path.exists():
|
|
152
|
+
return Check(
|
|
153
|
+
"config",
|
|
154
|
+
"warn",
|
|
155
|
+
f"no openagent.yaml at {config_path}",
|
|
156
|
+
"Create one by copying an example from docs/ or passing --config.",
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
import yaml
|
|
160
|
+
with open(config_path) as f:
|
|
161
|
+
yaml.safe_load(f)
|
|
162
|
+
return Check("config", "ok", f"{config_path}")
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return Check("config", "fail", f"invalid YAML: {e}", "Fix the syntax errors.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def check_memory_vault(config: dict) -> Check:
|
|
168
|
+
mem = config.get("memory", {}).get("vault_path") or "./memories"
|
|
169
|
+
p = Path(mem).expanduser()
|
|
170
|
+
if p.exists():
|
|
171
|
+
count = len(list(p.glob("*.md")))
|
|
172
|
+
return Check("memory-vault", "ok", f"{p} ({count} notes)")
|
|
173
|
+
return Check(
|
|
174
|
+
"memory-vault", "warn",
|
|
175
|
+
f"{p} does not exist yet",
|
|
176
|
+
f"Create with: mkdir -p {p}",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check_services_enabled(config: dict) -> list[Check]:
|
|
181
|
+
"""Check each enabled service has its prerequisites met."""
|
|
182
|
+
out: list[Check] = []
|
|
183
|
+
services = config.get("services", {}) or {}
|
|
184
|
+
for name, svc_cfg in services.items():
|
|
185
|
+
if not svc_cfg or not svc_cfg.get("enabled"):
|
|
186
|
+
continue
|
|
187
|
+
if name == "obsidian_web":
|
|
188
|
+
if not svc_cfg.get("password") and not os.environ.get("OBSIDIAN_PASSWORD"):
|
|
189
|
+
out.append(Check(
|
|
190
|
+
"service:obsidian_web", "fail",
|
|
191
|
+
"enabled but no password set",
|
|
192
|
+
"Set services.obsidian_web.password or OBSIDIAN_PASSWORD env var.",
|
|
193
|
+
))
|
|
194
|
+
else:
|
|
195
|
+
out.append(Check("service:obsidian_web", "ok", "configured"))
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── Doctor: run all checks ──
|
|
200
|
+
|
|
201
|
+
def run_doctor(config: dict, config_path: Path) -> Report:
|
|
202
|
+
"""Run the full set of checks and return a Report."""
|
|
203
|
+
rpt = Report()
|
|
204
|
+
rpt.add(check_python())
|
|
205
|
+
rpt.add(check_openagent_config(config_path))
|
|
206
|
+
rpt.add(check_memory_vault(config))
|
|
207
|
+
rpt.add(check_git())
|
|
208
|
+
rpt.add(check_node())
|
|
209
|
+
rpt.add(check_docker())
|
|
210
|
+
for c in check_services_enabled(config):
|
|
211
|
+
rpt.add(c)
|
|
212
|
+
return rpt
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── Installers ──
|
|
216
|
+
|
|
217
|
+
def _run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
|
|
218
|
+
logger.info("+ %s", " ".join(cmd))
|
|
219
|
+
return subprocess.run(cmd, check=check)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def install_docker_linux() -> str:
|
|
223
|
+
"""Install Docker via the distro's package manager."""
|
|
224
|
+
mgr = detect_linux_pkg_manager()
|
|
225
|
+
if mgr is None:
|
|
226
|
+
raise RuntimeError(
|
|
227
|
+
"No supported package manager found. Install Docker manually: "
|
|
228
|
+
"https://docs.docker.com/engine/install/"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
sudo = ["sudo"] if os.geteuid() != 0 else []
|
|
232
|
+
|
|
233
|
+
if mgr == "apt":
|
|
234
|
+
env = os.environ.copy()
|
|
235
|
+
env["DEBIAN_FRONTEND"] = "noninteractive"
|
|
236
|
+
subprocess.run(sudo + ["apt-get", "update", "-q"], check=True, env=env)
|
|
237
|
+
subprocess.run(sudo + ["apt-get", "install", "-y", "-q", "docker.io"], check=True, env=env)
|
|
238
|
+
elif mgr == "dnf":
|
|
239
|
+
_run(sudo + ["dnf", "install", "-y", "docker"])
|
|
240
|
+
elif mgr == "pacman":
|
|
241
|
+
_run(sudo + ["pacman", "-S", "--noconfirm", "docker"])
|
|
242
|
+
elif mgr == "zypper":
|
|
243
|
+
_run(sudo + ["zypper", "--non-interactive", "install", "docker"])
|
|
244
|
+
elif mgr == "apk":
|
|
245
|
+
_run(sudo + ["apk", "add", "docker"])
|
|
246
|
+
|
|
247
|
+
# Enable + start daemon (systemd)
|
|
248
|
+
if shutil.which("systemctl"):
|
|
249
|
+
subprocess.run(sudo + ["systemctl", "enable", "--now", "docker"], check=False)
|
|
250
|
+
|
|
251
|
+
# Add current user to docker group
|
|
252
|
+
user = os.environ.get("SUDO_USER") or os.environ.get("USER")
|
|
253
|
+
if user and user != "root":
|
|
254
|
+
subprocess.run(sudo + ["usermod", "-aG", "docker", user], check=False)
|
|
255
|
+
return (
|
|
256
|
+
f"Docker installed. User '{user}' added to the docker group — "
|
|
257
|
+
"you may need to log out and back in (or run `newgrp docker`) "
|
|
258
|
+
"before the current shell can use docker without sudo."
|
|
259
|
+
)
|
|
260
|
+
return "Docker installed and enabled."
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def install_docker_macos() -> str:
|
|
264
|
+
"""Install Docker Desktop on macOS via Homebrew cask if available."""
|
|
265
|
+
if shutil.which("docker"):
|
|
266
|
+
return "Docker already installed."
|
|
267
|
+
|
|
268
|
+
if shutil.which("brew"):
|
|
269
|
+
try:
|
|
270
|
+
_run(["brew", "install", "--cask", "docker"])
|
|
271
|
+
return (
|
|
272
|
+
"Docker Desktop installed via Homebrew. "
|
|
273
|
+
"Launch the Docker app from /Applications once to accept the "
|
|
274
|
+
"terms of service and finish setup, then re-run "
|
|
275
|
+
"`openagent doctor` to verify."
|
|
276
|
+
)
|
|
277
|
+
except subprocess.CalledProcessError as e:
|
|
278
|
+
raise RuntimeError(f"brew install failed: {e}")
|
|
279
|
+
|
|
280
|
+
raise RuntimeError(
|
|
281
|
+
"Homebrew not found. Install it from https://brew.sh then re-run "
|
|
282
|
+
"this command, or install Docker Desktop manually from "
|
|
283
|
+
"https://docs.docker.com/desktop/install/mac-install/"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def install_docker_windows() -> str:
|
|
288
|
+
"""Install Docker Desktop on Windows via winget if available."""
|
|
289
|
+
if shutil.which("docker"):
|
|
290
|
+
return "Docker already installed."
|
|
291
|
+
|
|
292
|
+
if shutil.which("winget"):
|
|
293
|
+
try:
|
|
294
|
+
_run([
|
|
295
|
+
"winget", "install", "--silent", "--accept-source-agreements",
|
|
296
|
+
"--accept-package-agreements", "Docker.DockerDesktop",
|
|
297
|
+
])
|
|
298
|
+
return (
|
|
299
|
+
"Docker Desktop installed via winget. A reboot is usually "
|
|
300
|
+
"required. After rebooting, launch Docker Desktop once, then "
|
|
301
|
+
"re-run `openagent doctor`."
|
|
302
|
+
)
|
|
303
|
+
except subprocess.CalledProcessError as e:
|
|
304
|
+
raise RuntimeError(f"winget install failed: {e}")
|
|
305
|
+
|
|
306
|
+
raise RuntimeError(
|
|
307
|
+
"winget not found. Install Docker Desktop manually from "
|
|
308
|
+
"https://docs.docker.com/desktop/install/windows-install/"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def install_docker() -> str:
|
|
313
|
+
"""Dispatch to the platform-specific installer."""
|
|
314
|
+
pf = current_platform()
|
|
315
|
+
if pf == "linux":
|
|
316
|
+
return install_docker_linux()
|
|
317
|
+
if pf == "macos":
|
|
318
|
+
return install_docker_macos()
|
|
319
|
+
if pf == "windows":
|
|
320
|
+
return install_docker_windows()
|
|
321
|
+
raise RuntimeError(f"Unsupported platform for automatic Docker install: {pf}")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ── Image pulling ──
|
|
325
|
+
|
|
326
|
+
SERVICE_IMAGES = {
|
|
327
|
+
"obsidian_web": "lscr.io/linuxserver/obsidian:latest",
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def enabled_service_images(config: dict) -> dict[str, str]:
|
|
332
|
+
out: dict[str, str] = {}
|
|
333
|
+
services = config.get("services", {}) or {}
|
|
334
|
+
for name, cfg in services.items():
|
|
335
|
+
if cfg and cfg.get("enabled") and name in SERVICE_IMAGES:
|
|
336
|
+
out[name] = SERVICE_IMAGES[name]
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
async def pull_service_images(config: dict) -> list[tuple[str, bool, str]]:
|
|
341
|
+
"""Pull Docker images for every enabled aux service.
|
|
342
|
+
|
|
343
|
+
Returns a list of (service_name, success, message) tuples.
|
|
344
|
+
"""
|
|
345
|
+
out: list[tuple[str, bool, str]] = []
|
|
346
|
+
if not shutil.which("docker"):
|
|
347
|
+
return out
|
|
348
|
+
|
|
349
|
+
for name, image in enabled_service_images(config).items():
|
|
350
|
+
logger.info(f"Pulling image for {name}: {image}")
|
|
351
|
+
try:
|
|
352
|
+
proc = await asyncio.create_subprocess_exec(
|
|
353
|
+
"docker", "pull", image,
|
|
354
|
+
stdout=asyncio.subprocess.PIPE,
|
|
355
|
+
stderr=asyncio.subprocess.PIPE,
|
|
356
|
+
)
|
|
357
|
+
_, err = await proc.communicate()
|
|
358
|
+
if proc.returncode == 0:
|
|
359
|
+
out.append((name, True, f"pulled {image}"))
|
|
360
|
+
else:
|
|
361
|
+
out.append((name, False, f"pull failed: {err.decode(errors='replace').strip()[:200]}"))
|
|
362
|
+
except Exception as e:
|
|
363
|
+
out.append((name, False, f"pull error: {e}"))
|
|
364
|
+
return out
|
|
@@ -331,41 +331,152 @@ def services_cmd(ctx, action: str):
|
|
|
331
331
|
asyncio.run(_run())
|
|
332
332
|
|
|
333
333
|
|
|
334
|
+
# ── Doctor: environment checks ──
|
|
335
|
+
|
|
336
|
+
_STATUS_STYLE = {
|
|
337
|
+
"ok": "[green]✓[/green]",
|
|
338
|
+
"warn": "[yellow]![/yellow]",
|
|
339
|
+
"fail": "[red]✗[/red]",
|
|
340
|
+
"skip": "[dim]·[/dim]",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _print_report(report) -> None:
|
|
345
|
+
from rich.table import Table as _Table
|
|
346
|
+
tbl = _Table(show_header=True, header_style="bold")
|
|
347
|
+
tbl.add_column("", width=2)
|
|
348
|
+
tbl.add_column("Check", style="cyan")
|
|
349
|
+
tbl.add_column("Status")
|
|
350
|
+
tbl.add_column("Fix", style="dim")
|
|
351
|
+
for c in report.checks:
|
|
352
|
+
icon = _STATUS_STYLE.get(c.status, "?")
|
|
353
|
+
tbl.add_row(icon, c.name, c.message, c.fix_hint or "")
|
|
354
|
+
console.print(tbl)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@main.command("doctor")
|
|
358
|
+
@click.pass_context
|
|
359
|
+
def doctor_cmd(ctx):
|
|
360
|
+
"""Check the environment: Python, Docker, config, enabled services."""
|
|
361
|
+
from pathlib import Path
|
|
362
|
+
from openagent.bootstrap import run_doctor, current_platform
|
|
363
|
+
|
|
364
|
+
config = ctx.obj["config"]
|
|
365
|
+
config_path = Path(ctx.obj["config_path"]).expanduser()
|
|
366
|
+
|
|
367
|
+
console.print(f"[bold]Platform:[/bold] {current_platform()}")
|
|
368
|
+
console.print()
|
|
369
|
+
|
|
370
|
+
report = run_doctor(config, config_path)
|
|
371
|
+
_print_report(report)
|
|
372
|
+
|
|
373
|
+
console.print()
|
|
374
|
+
if report.has_failures:
|
|
375
|
+
console.print("[red]Some checks failed.[/red] Fix the issues above and re-run `openagent doctor`.")
|
|
376
|
+
raise SystemExit(1)
|
|
377
|
+
if report.has_warnings:
|
|
378
|
+
console.print("[yellow]All critical checks passed, with warnings.[/yellow]")
|
|
379
|
+
else:
|
|
380
|
+
console.print("[green]All checks passed. You're good to go.[/green]")
|
|
381
|
+
|
|
382
|
+
|
|
334
383
|
# ── Service management (OS-level systemd/launchd) ──
|
|
335
384
|
|
|
336
385
|
@main.command("setup")
|
|
386
|
+
@click.option("--with-docker", is_flag=True,
|
|
387
|
+
help="Install Docker (Linux: via apt/dnf/pacman; Mac/Win: via brew/winget).")
|
|
388
|
+
@click.option("--pull-images", is_flag=True,
|
|
389
|
+
help="Pre-pull Docker images for every enabled aux service.")
|
|
390
|
+
@click.option("--full", is_flag=True,
|
|
391
|
+
help="Everything: doctor, install Docker if missing, pull images, register OS service.")
|
|
392
|
+
@click.option("--no-service", is_flag=True,
|
|
393
|
+
help="Skip OS service registration (systemd/launchd/Task Scheduler).")
|
|
337
394
|
@click.pass_context
|
|
338
|
-
def setup_cmd(ctx):
|
|
339
|
-
"""
|
|
340
|
-
|
|
395
|
+
def setup_cmd(ctx, with_docker: bool, pull_images: bool, full: bool, no_service: bool):
|
|
396
|
+
"""First-time setup: check environment, install deps, register OS service.
|
|
397
|
+
|
|
398
|
+
By default only registers OpenAgent as an OS service. Pass --full to also
|
|
399
|
+
install Docker (where automatable) and pre-pull images for the services
|
|
400
|
+
enabled in your config.
|
|
401
|
+
"""
|
|
402
|
+
from pathlib import Path
|
|
403
|
+
from openagent.bootstrap import (
|
|
404
|
+
run_doctor, install_docker, pull_service_images,
|
|
405
|
+
check_docker, current_platform,
|
|
406
|
+
)
|
|
341
407
|
from openagent.service import setup_service
|
|
342
408
|
|
|
343
|
-
|
|
344
|
-
|
|
409
|
+
config = ctx.obj["config"]
|
|
410
|
+
config_path = Path(ctx.obj["config_path"]).expanduser()
|
|
411
|
+
|
|
412
|
+
if full:
|
|
413
|
+
with_docker = True
|
|
414
|
+
pull_images = True
|
|
415
|
+
|
|
416
|
+
console.print(f"[bold]Platform:[/bold] {current_platform()}")
|
|
345
417
|
console.print()
|
|
346
418
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
419
|
+
# 1. Doctor pass 1
|
|
420
|
+
console.print("[bold]Step 1 — environment check[/bold]")
|
|
421
|
+
report = run_doctor(config, config_path)
|
|
422
|
+
_print_report(report)
|
|
423
|
+
console.print()
|
|
424
|
+
|
|
425
|
+
# 2. Install Docker if requested and missing
|
|
426
|
+
if with_docker:
|
|
427
|
+
console.print("[bold]Step 2 — Docker[/bold]")
|
|
428
|
+
docker_chk = check_docker()
|
|
429
|
+
if docker_chk.status == "ok":
|
|
430
|
+
console.print(f"[green]Docker already OK:[/green] {docker_chk.message}")
|
|
431
|
+
else:
|
|
432
|
+
try:
|
|
433
|
+
msg = install_docker()
|
|
434
|
+
console.print(f"[green]{msg}[/green]")
|
|
435
|
+
except Exception as e:
|
|
436
|
+
console.print(f"[red]Docker install failed:[/red] {e}")
|
|
351
437
|
console.print()
|
|
352
|
-
|
|
353
|
-
|
|
438
|
+
|
|
439
|
+
# 3. Pull images for enabled services
|
|
440
|
+
if pull_images:
|
|
441
|
+
console.print("[bold]Step 3 — pulling service images[/bold]")
|
|
442
|
+
results = asyncio.run(pull_service_images(config))
|
|
443
|
+
if not results:
|
|
444
|
+
console.print("[dim]No services with pullable images are enabled.[/dim]")
|
|
445
|
+
else:
|
|
446
|
+
for name, ok, msg in results:
|
|
447
|
+
icon = "[green]✓[/green]" if ok else "[red]✗[/red]"
|
|
448
|
+
console.print(f" {icon} {name}: {msg}")
|
|
354
449
|
console.print()
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
|
|
450
|
+
|
|
451
|
+
# 4. Register OS service
|
|
452
|
+
if not no_service:
|
|
453
|
+
console.print("[bold]Step 4 — register OS service[/bold]")
|
|
454
|
+
try:
|
|
455
|
+
info = setup_service()
|
|
456
|
+
console.print(f"[green]{info['message']}[/green]")
|
|
457
|
+
console.print(f"[dim]Service file:[/dim] {info['service_file']}")
|
|
458
|
+
except Exception as e:
|
|
459
|
+
console.print(f"[red]Service registration failed:[/red] {e}")
|
|
460
|
+
raise SystemExit(1)
|
|
461
|
+
console.print()
|
|
462
|
+
|
|
463
|
+
# 5. Final re-check
|
|
464
|
+
console.print("[bold]Final check[/bold]")
|
|
465
|
+
final = run_doctor(config, config_path)
|
|
466
|
+
_print_report(final)
|
|
467
|
+
console.print()
|
|
468
|
+
|
|
469
|
+
if final.has_failures:
|
|
470
|
+
console.print("[red]Setup finished with failures.[/red] See above.")
|
|
361
471
|
raise SystemExit(1)
|
|
472
|
+
console.print("[green]Setup complete.[/green]")
|
|
362
473
|
|
|
363
474
|
|
|
364
475
|
@main.command("install")
|
|
365
476
|
@click.pass_context
|
|
366
477
|
def install_cmd(ctx):
|
|
367
|
-
"""Alias for openagent setup
|
|
368
|
-
ctx.invoke(setup_cmd)
|
|
478
|
+
"""Alias for `openagent setup --full`."""
|
|
479
|
+
ctx.invoke(setup_cmd, with_docker=True, pull_images=True, full=True, no_service=False)
|
|
369
480
|
|
|
370
481
|
|
|
371
482
|
@main.command("uninstall")
|
|
@@ -69,24 +69,29 @@ class Scheduler:
|
|
|
69
69
|
logger.error(f"Scheduler error: {e}")
|
|
70
70
|
await asyncio.sleep(CHECK_INTERVAL)
|
|
71
71
|
|
|
72
|
+
async def run_task(self, task: dict) -> None:
|
|
73
|
+
"""Execute a single task. Extension point: override or monkey-patch
|
|
74
|
+
this to intercept specific tasks (e.g. auto-update, which uses a
|
|
75
|
+
direct pip subprocess instead of going through the agent)."""
|
|
76
|
+
task_name = task["name"]
|
|
77
|
+
try:
|
|
78
|
+
response = await self.agent.run(
|
|
79
|
+
message=task["prompt"],
|
|
80
|
+
user_id="scheduler",
|
|
81
|
+
session_id=f"scheduler:{task['id']}",
|
|
82
|
+
)
|
|
83
|
+
logger.info(f"Task '{task_name}' completed: {response[:100]}...")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Task '{task_name}' failed: {e}")
|
|
86
|
+
|
|
72
87
|
async def _check_and_run(self) -> None:
|
|
73
88
|
"""Check for due tasks and execute them."""
|
|
74
89
|
now = time.time()
|
|
75
90
|
due_tasks = await self.db.get_due_tasks(now)
|
|
76
91
|
|
|
77
92
|
for task in due_tasks:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
try:
|
|
82
|
-
response = await self.agent.run(
|
|
83
|
-
message=task["prompt"],
|
|
84
|
-
user_id="scheduler",
|
|
85
|
-
session_id=f"scheduler:{task['id']}",
|
|
86
|
-
)
|
|
87
|
-
logger.info(f"Task '{task_name}' completed: {response[:100]}...")
|
|
88
|
-
except Exception as e:
|
|
89
|
-
logger.error(f"Task '{task_name}' failed: {e}")
|
|
93
|
+
logger.info(f"Running scheduled task: {task['name']}")
|
|
94
|
+
await self.run_task(task)
|
|
90
95
|
|
|
91
96
|
# Update last_run and compute next_run
|
|
92
97
|
try:
|
|
@@ -98,7 +103,7 @@ class Scheduler:
|
|
|
98
103
|
next_run=next_run,
|
|
99
104
|
)
|
|
100
105
|
except (ValueError, KeyError) as e:
|
|
101
|
-
logger.error(f"Failed to update next_run for '{
|
|
106
|
+
logger.error(f"Failed to update next_run for '{task['name']}': {e}")
|
|
102
107
|
|
|
103
108
|
# ── Task management helpers ──
|
|
104
109
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openagent-framework"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Simplified LLM agent framework with MCP, memory, and multi-channel support"
|
|
9
9
|
readme = "docs/README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/chrome-devtools/.gitignore
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/chrome-devtools/package.json
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/.gitignore
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/package.json
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/index.ts
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/computer-control/src/main.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/package.json
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/messaging/tsconfig.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/.gitignore
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/package.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/index.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/types.ts
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/src/utils.ts
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent/mcps/web-search/tsconfig.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/requires.txt
RENAMED
|
File without changes
|
{openagent_framework-0.2.1 → openagent_framework-0.2.3}/openagent_framework.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|